copious-fedex 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,6 @@
1
+ .DS_Store
2
+ .svn
3
+ lib/wsdl
4
+ *.sw?
5
+ fedex.gemspec
6
+ pkg/
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2007 Joseph Jaramillo
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
@@ -0,0 +1,157 @@
1
+ = fedex
2
+
3
+ == Description
4
+
5
+ This Rails[http://www.rubyonrails.org] plugin will enable you to integrate with Fedex's Web Services platform for the purpose of obtaining shipping rate quotes, generating shipping labels, and cancelling shipments. Web Services is Fedex's new web service platform that replaces Web Integration. It is quite robust. The plugin we're providing here attempts to make using this new system as simple as you've come to expect from Rails itself. It does not implement Web Services in its entirety, but it provides the two core services (Rate and Ship) most applicable to e-commerce.
6
+
7
+ This is a fork of {Mighty Interactive's Fedex plugin}[http://mightyinteractive.com/ruby-on-rails/fedex/] that has been updated to be compatible with the latest version (v7 as of 10/22/2009) of the Fedex API.
8
+
9
+ === Using as a gem
10
+
11
+ (I'll be revising the Installation section below, but this is how to do it now:)
12
+
13
+ Insert this in config/environment.rb, just like when using this as a plugin:
14
+
15
+ config.gem "soap4r", :lib => "soap/soap", :version => ">= 1.5.8"
16
+
17
+ And also this:
18
+
19
+ config.gem "fedex"
20
+
21
+ Get your hands on the v8 WSDL files and toss them in lib/fedex_wsdl/ in your application. You'll need to use an initializer to tell the gem where to look.
22
+
23
+ mkdir config/initializers
24
+ cat >> config/initializers/fedex_wsdl.rb
25
+ Fedex::Base::WSDL_PATHS.merge!({
26
+ :rate => "#{RAILS_ROOT}/lib/fedex_wsdl/RateService_v8.wsdl",
27
+ :ship => "#{RAILS_ROOT}/lib/fedex_wsdl/ShipService_v8.wsdl",
28
+ })
29
+ ^D
30
+
31
+ That should do it.
32
+
33
+ === Installation
34
+
35
+ This plugin depends upon NaHi's[http://dev.ctor.org/soap4r/wiki/NaHi] excellent SOAP4R[http://dev.ctor.org/soap4r] library. Just add this line to <tt>config/environment.rb</tt>:
36
+
37
+ config.gem "soap4r", :lib => "soap/soap", :version => ">= 1.5.8"
38
+
39
+ and then run:
40
+
41
+ $ sudo rake gems:install
42
+
43
+ and then if you want to:
44
+
45
+ $ rake gems:unpack
46
+
47
+ To install the plugin itself, simply navigate to your project directory and run:
48
+
49
+ script/plugin install git://github.com/mcmire/fedex.git
50
+
51
+ Due to copyright reasons we cannot distribute the associated WSDL files; you will need to apply for a {developer account}[http://www.fedex.com/developer] with Fedex to begin working on your integration. Once you've created your account, head to the "Get Started" section, where you can find documentation and the individual WSDLs for all of the available services. For our purposes you need only two: Rate (<tt>RateService_v7.wsdl</tt> as of 10/22/2009) and Ship (<tt>ShipService_v7.wsdl</tt> as of 10/22/2009). Download these WSDLs and put them in the <tt>vendor/plugins/fedex/lib/wsdl/</tt> directory.
52
+
53
+ == Usage
54
+
55
+ Using the plugin is straightforward:
56
+
57
+ Start out by defining constants to hold the authentication parameters. To use Fedex Web Services you will need four pieces of information: Account Number, Authorization Key, Security Code, and Meter Number. You will receive all four when you create your developer account. An ideal place to put these constants is in an initializer under <tt>config/initializers</tt>.
58
+
59
+ AUTH_KEY = 'YOUR_AUTHORIZATION_KEY'
60
+ SECURITY_CODE = 'YOUR_SECURITY_CODE'
61
+ ACCOUNT_NUMBER = 'YOUR_ACCOUNT_NUMBER'
62
+ METER_NUMBER = 'YOUR_METER_NUMBER'
63
+
64
+ Before you can get a rate or create a label, you must first create a Fedex object. Here you pass in the constants you just created, along with any other options that apply (see <tt>lib/fedex.rb</tt>).
65
+
66
+ fedex = Fedex::Base.new(
67
+ :auth_key => AUTH_KEY,
68
+ :security_code => SECURITY_CODE,
69
+ :account_number => ACCOUNT_NUMBER,
70
+ :meter_number => METER_NUMBER
71
+ )
72
+
73
+ Note that leaving out one or more required pieces of information for any method will result in an exception being thrown:
74
+
75
+ > fedex = Fedex::Base.new
76
+ Fedex::MissingInformationError: Missing :auth_key, :security_code, :account_number, :meter_number
77
+ from ./lib/fedex.rb:204:in `check_required_options'
78
+ from ./lib/fedex.rb:37:in `initialize'
79
+
80
+ For the purpose of demonstration we're using the PDF label type, which is the default. PDFs are nice because they'll print onto a regular 8.5"x11" sheet of paper exactly the way Fedex needs them. Additional options for printing are available. See <tt>Fedex::LabelSpecificationImageTypes</tt> (defined in <tt>lib/{rate|ship}_constants.rb</tt>) for a list, which includes PNG and special formats designed for thermal printers.
81
+
82
+ Now let's get a Rate quote. Define your origin, destination, number of packages, total weight, and shipping method.
83
+
84
+ shipper = {
85
+ :name => "Your Name",
86
+ :phone_number => '5205551212'
87
+ }
88
+ recipient = {
89
+ :name => "Fedex",
90
+ :phone_number => '9013693600'
91
+ }
92
+ origin = {
93
+ :street => '80 E. Rio Salado Pkwy. #711', # Off Madison Ave
94
+ :city => 'Tempe',
95
+ :state => 'AZ',
96
+ :zip => '85281',
97
+ :country => 'US'
98
+ }
99
+ destination = {
100
+ :street => '942 South Shady Grove Road', # Fedex
101
+ :city => 'Memphis',
102
+ :state => 'TN',
103
+ :zip => '38120',
104
+ :country => 'US',
105
+ :residential => false
106
+ }
107
+ pkg_count = 1
108
+ weight = 10
109
+ service_type = Fedex::ServiceTypes::STANDARD_OVERNIGHT
110
+
111
+ Pass these to your Fedex object:
112
+
113
+ price = fedex.price(
114
+ :shipper => { :contact => shipper, :address => origin },
115
+ :recipient => { :contact => recipient, :address => destination },
116
+ :count => pkg_count,
117
+ :weight => weight,
118
+ :service_type => service_type
119
+ )
120
+ price #=> 8644
121
+
122
+ Note that rate quotes are returned as whole integers in cents (so the charge in this case is $86.44).
123
+
124
+ Shipping is just as easy:
125
+
126
+ price, label, tracking_number = fedex.label(
127
+ :shipper => { :contact => shipper, :address => origin },
128
+ :recipient => { :contact => recipient, :address => destination },
129
+ :count => pkg_count,
130
+ :weight => weight,
131
+ :service_type => service_type
132
+ )
133
+
134
+ If everything goes well, +price+, +label+, and +tracking_number+ will all be populated accordingly. +label+ is the Base64-decoded label as returned from Fedex. By default the Fedex plugin requests the label to be returned as a PDF file suitable for laser printing. Store this in a <tt>:binary</tt> column in your database, or write it out to a file.
135
+
136
+ And that's it! There are quite a few additional configuration options which you can find by looking in the documentation in the source code itself, but this should be enough to get you started.
137
+
138
+ == Support
139
+
140
+ I (Elliot) use this plugin at work and right now we are only using the 'price' feature of this plugin. However, I realize that other people may be using the other features. If you encounter any bugs while using this, I am happy to fix the plugin for you; however, I rely on you to give me as much information as possible to do so. You can help me by:
141
+
142
+ * going to the 'Issues' tab in Github[http://github.com/mcmire/fedex] and adding an issue
143
+ * creating the patch yourself and sending me a pull request
144
+ * sending me an email (elliot.winkler [at] gmail [dot] com)
145
+
146
+ == Author/Contributors
147
+
148
+ * Joseph Jamarillo, josephj [at] offmadisonave [dot] com (original author)
149
+ * Elliot Winkler, elliot.winkler [at] gmail [dot] com (fork for v5 compatibility)
150
+ * Laurence A. Lee, lalee [at] pobox [dot] com (additional fixes for v5 compatibility)
151
+ * Matthew Boeh, matt [at] copiousinc [dot] com (janitorial work)
152
+
153
+ == Copyright/License
154
+
155
+ Copyright (c) 2007 Joseph Jaramillo
156
+
157
+ This plugin is made available under the MIT license.
@@ -0,0 +1,37 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+
5
+ desc 'Default: run unit tests.'
6
+ task :default => :test
7
+
8
+ desc 'Test the fedex plugin.'
9
+ Rake::TestTask.new(:test) do |t|
10
+ t.libs << 'lib'
11
+ t.pattern = 'test/**/*_test.rb'
12
+ t.verbose = true
13
+ end
14
+
15
+ desc 'Generate documentation for the fedex plugin.'
16
+ Rake::RDocTask.new(:rdoc) do |rdoc|
17
+ rdoc.rdoc_dir = 'rdoc'
18
+ rdoc.title = 'Fedex'
19
+ rdoc.options << '--line-numbers' << '--inline-source'
20
+ rdoc.rdoc_files.include('README')
21
+ rdoc.rdoc_files.include('lib/**/*.rb')
22
+ end
23
+
24
+ begin
25
+ require 'rubygems'
26
+ require 'jeweler'
27
+ Jeweler::Tasks.new do |gemspec|
28
+ gemspec.name = "copious-fedex"
29
+ gemspec.summary = "Retrieve shipping quotes, generate labels, and cancel shipments with the FedEx v8 web service."
30
+ gemspec.description = "Retrieve shipping quotes, generate labels, and cancel shipments with the FedEx v8 web service."
31
+ gemspec.email = "matt@copiousinc.com"
32
+ gemspec.homepage = "http://github.com/copious/fedex"
33
+ gemspec.authors = ["Joseph Jamarillo", "Elliot Winkler", "Laurence A. Lee", "Matthew Boeh"]
34
+ end
35
+ rescue LoadError
36
+ puts "Jeweler not available. Install it with: gem install jeweler"
37
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.1.0
@@ -0,0 +1,475 @@
1
+ # Copyright (c) 2007 Joseph Jaramillo
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ require 'fedex/rate_constants'
22
+ require 'fedex/ship_constants'
23
+
24
+ module Fedex #:nodoc:
25
+
26
+ class MissingInformationError < StandardError; end #:nodoc:
27
+ class FedexError < StandardError; end #:nodoc
28
+
29
+ # Provides access to Fedex Web Services
30
+ class Base
31
+
32
+ # Defines the required parameters for various methods
33
+ REQUIRED_OPTIONS = {
34
+ :base => [ :auth_key, :security_code, :account_number, :meter_number ],
35
+ :price => [ :shipper, :recipient, :weight ],
36
+ :label => [ :shipper, :recipient, :weight, :service_type ],
37
+ :contact => [ :name, :phone_number ],
38
+ :address => [ :country, :street, :city, :state, :zip ],
39
+ :ship_cancel => [ :tracking_number ]
40
+ }
41
+
42
+ # Defines the relative path to the WSDL files. Defaults assume lib/wsdl under plugin directory.
43
+ WSDL_PATHS = {
44
+ :rate => 'wsdl/RateService_v8.wsdl',
45
+ :ship => 'wsdl/ShipService_v8.wsdl',
46
+ }
47
+
48
+ # Defines the Web Services version implemented.
49
+ WS_VERSION = { :Major => 8, :Intermediate => 0, :Minor => 0 }
50
+
51
+ SUCCESSFUL_RESPONSES = ['SUCCESS', 'WARNING', 'NOTE'] #:nodoc:
52
+
53
+ DIR = File.dirname(__FILE__)
54
+
55
+ attr_accessor :auth_key,
56
+ :security_code,
57
+ :account_number,
58
+ :meter_number,
59
+ :dropoff_type,
60
+ :service_type,
61
+ :units,
62
+ :packaging_type,
63
+ :sender,
64
+ :debug
65
+
66
+ # Initializes the Fedex::Base class, setting defaults where necessary.
67
+ #
68
+ # fedex = Fedex::Base.new(options = {})
69
+ #
70
+ # === Example:
71
+ # fedex = Fedex::Base.new(:auth_key => AUTH_KEY,
72
+ # :security_code => SECURITY_CODE
73
+ # :account_number => ACCOUNT_NUMBER,
74
+ # :meter_number => METER_NUMBER)
75
+ #
76
+ # === Required options for new
77
+ # :auth_key - Your Fedex Authorization Key
78
+ # :security_code - Your Fedex Security Code
79
+ # :account_number - Your Fedex Account Number
80
+ # :meter_number - Your Fedex Meter Number
81
+ #
82
+ # === Additional options
83
+ # :dropoff_type - One of Fedex::DropoffTypes. Defaults to DropoffTypes::REGULAR_PICKUP
84
+ # :packaging_type - One of Fedex::PackagingTypes. Defaults to PackagingTypes::YOUR_PACKAGING
85
+ # :label_type - One of Fedex::LabelFormatTypes. Defaults to LabelFormatTypes::COMMON2D. You'll only need to change this
86
+ # if you're generating completely custom labels with a format of your own design. If printing to Fedex stock
87
+ # leave this alone.
88
+ # :label_image_type - One of Fedex::LabelSpecificationImageTypes. Defaults to LabelSpecificationImageTypes::PDF.
89
+ # :rate_request_type - One of Fedex::RateRequestTypes. Defaults to RateRequestTypes::ACCOUNT
90
+ # :payment - One of Fedex::PaymentTypes. Defaults to PaymentTypes::SENDER
91
+ # :units - One of Fedex::WeightUnits. Defaults to WeightUnits::LB
92
+ # :currency - One of Fedex::CurrencyTypes. Defaults to CurrencyTypes::USD
93
+ # :debug - Enable or disable debug (wiredump) output. Defaults to false.
94
+ def initialize(options = {})
95
+ check_required_options(:base, options)
96
+
97
+ @auth_key = options[:auth_key]
98
+ @security_code = options[:security_code]
99
+ @account_number = options[:account_number]
100
+ @meter_number = options[:meter_number]
101
+
102
+ @dropoff_type = options[:dropoff_type] || DropoffTypes::REGULAR_PICKUP
103
+ @packaging_type = options[:packaging_type] || PackagingTypes::YOUR_PACKAGING
104
+ @label_type = options[:label_type] || LabelFormatTypes::COMMON2D
105
+ @label_image_type = options[:label_image_type] || LabelSpecificationImageTypes::PDF
106
+ @rate_request_type = options[:rate_request_type] || RateRequestTypes::LIST
107
+ @payment_type = options[:payment] || PaymentTypes::SENDER
108
+ @units = options[:units] || WeightUnits::LB
109
+ @currency = options[:currency] || CurrencyTypes::USD
110
+ @debug = options[:debug] || false
111
+ end
112
+
113
+ # Gets a rate quote from Fedex.
114
+ #
115
+ # fedex = Fedex::Base.new(options)
116
+ #
117
+ # single_price = fedex.price(
118
+ # :shipper => { ... },
119
+ # :recipient => { ... },
120
+ # :weight => 1,
121
+ # :service_type => 'STANDARD_OVERNIGHT'
122
+ # }
123
+ # single_price #=> 1315
124
+ #
125
+ # multiple_prices = fedex.price(
126
+ # :shipper => { ... },
127
+ # :recipient => { ... },
128
+ # :weight => 1
129
+ # )
130
+ # multiple_prices #=> { 'STANDARD_OVERNIGHT' => 1315, 'PRIORITY_OVERNIGHT' => 2342, ... }
131
+ #
132
+ # === Required options for price
133
+ # :shipper - A hash containing contact information and an address for the shipper. (See below.)
134
+ # :recipient - A hash containing contact information and an address for the recipient. (See below.)
135
+ # :weight - The total weight of the shipped package.
136
+ #
137
+ # === Optional options
138
+ # :count - How many packages are in the shipment. Defaults to 1.
139
+ # :service_type - One of Fedex::ServiceTypes. If not specified, Fedex gives you rates for all
140
+ # of the available service types (and you will receive a hash of prices instead of a
141
+ # single price).
142
+ #
143
+ # === Address format
144
+ # The 'shipper' and 'recipient' address values should be hashes. Like this:
145
+ #
146
+ # address = {
147
+ # :country => 'US',
148
+ # :street => '1600 Pennsylvania Avenue NW'
149
+ # :city => 'Washington',
150
+ # :state => 'DC',
151
+ # :zip => '20500'
152
+ # }
153
+ def price(options = {})
154
+ puts options.inspect if $DEBUG
155
+
156
+ # Check overall options
157
+ check_required_options(:price, options)
158
+
159
+ # Check Address Options
160
+ check_required_options(:contact, options[:shipper][:contact])
161
+ check_required_options(:address, options[:shipper][:address])
162
+
163
+ # Check Contact Options
164
+ check_required_options(:contact, options[:recipient][:contact])
165
+ check_required_options(:address, options[:recipient][:address])
166
+
167
+ # Build shipment options
168
+ options = build_shipment_options(:crs, options)
169
+
170
+ # Process the rate request
171
+ driver = create_driver(:rate)
172
+ result = driver.getRates(options)
173
+
174
+ extract_price = proc do |reply_detail|
175
+ shipment_details = reply_detail.ratedShipmentDetails
176
+ price = nil
177
+ for shipment_detail in shipment_details
178
+ rate_detail = shipment_detail.shipmentRateDetail
179
+ if rate_detail.rateType == "PAYOR_#{@rate_request_type}"
180
+ price = (rate_detail.totalNetCharge.amount.to_f * 100).to_i
181
+ break
182
+ end
183
+ end
184
+ if price
185
+ return price
186
+ else
187
+ raise "Couldn't find Fedex price in response!"
188
+ end
189
+ end
190
+
191
+ msg = error_msg(result, false)
192
+ if successful?(result) && msg !~ /There are no valid services available/
193
+ reply_details = result.rateReplyDetails
194
+ if reply_details.respond_to?(:ratedShipmentDetails)
195
+ price = extract_price.call(reply_details)
196
+ service_type ? price : { reply_details.serviceType => price }
197
+ else
198
+ reply_details.inject({}) {|h,r| h[r.serviceType] = extract_price.call(r); h }
199
+ end
200
+ else
201
+ raise FedexError.new("Unable to retrieve price from Fedex: #{msg}")
202
+ end
203
+ end
204
+
205
+ # Generate a new shipment and return associated data, including price, tracking number, and the label itself.
206
+ #
207
+ # fedex = Fedex::Base.new(options)
208
+ # price, label, tracking_number = fedex.label(fields)
209
+ #
210
+ # Returns the actual price for the label, the Base64-decoded label in the format specified in Fedex::Base,
211
+ # and the tracking_number for the shipment.
212
+ #
213
+ # === Required options for label
214
+ # :shipper - A hash containing contact information and an address for the shipper. (See below.)
215
+ # :recipient - A hash containing contact information and an address for the recipient. (See below.)
216
+ # :weight - The total weight of the shipped package.
217
+ # :service_type - One of Fedex::ServiceTypes
218
+ #
219
+ # === Address format
220
+ # The 'shipper' and 'recipient' address values should be hashes. Like this:
221
+ #
222
+ # shipper = {:contact => {:name => 'John Doe',
223
+ # :phone_number => '4805551212'},
224
+ # :address => address} # See "Address" for under price.
225
+ def label(options = {})
226
+ puts options.inspect if $DEBUG
227
+
228
+ # Check overall options
229
+ check_required_options(:label, options)
230
+
231
+ # Check Address Options
232
+ check_required_options(:contact, options[:shipper][:contact])
233
+ check_required_options(:address, options[:shipper][:address])
234
+
235
+ # Check Contact Options
236
+ check_required_options(:contact, options[:recipient][:contact])
237
+ check_required_options(:address, options[:recipient][:address])
238
+
239
+ # Build shipment options
240
+ options = build_shipment_options(:ship, options)
241
+
242
+ # Process the shipment request
243
+ driver = create_driver(:ship)
244
+ result = driver.processShipment(options)
245
+ successful = successful?(result)
246
+
247
+ msg = error_msg(result, false)
248
+ if successful && msg !~ /There are no valid services available/
249
+ pre = result.completedShipmentDetail.shipmentRating.shipmentRateDetails
250
+ charge = ((pre.class == Array ? pre[0].totalNetCharge.amount.to_f : pre.totalNetCharge.amount.to_f) * 100).to_i
251
+ label = Base64.decode64(result.completedShipmentDetail.completedPackageDetails.label.parts.image)
252
+ tracking_number = result.completedShipmentDetail.completedPackageDetails.trackingIds.trackingNumber
253
+ [charge, label, tracking_number]
254
+ else
255
+ raise FedexError.new("Unable to get label from Fedex: #{msg}")
256
+ end
257
+ end
258
+
259
+ # Cancel a shipment
260
+ #
261
+ # fedex = Fedex::Base.new(options)
262
+ # result = fedex.cancel(options)
263
+ #
264
+ # Returns a boolean indicating whether or not the operation was successful
265
+ #
266
+ # === Required options for cancel
267
+ # :tracking_number - The Fedex-provided tracking number you wish to cancel
268
+ def cancel(options = {})
269
+ check_required_options(:ship_cancel, options)
270
+
271
+ tracking_number = options[:tracking_number]
272
+ #carrier_code = options[:carrier_code] || carrier_code_for_tracking_number(tracking_number)
273
+
274
+ driver = create_driver(:ship)
275
+
276
+ result = driver.deleteShipment(common_options(:ship).merge(
277
+ :TrackingNumber => tracking_number
278
+ ))
279
+
280
+ return successful?(result)
281
+ end
282
+
283
+ private
284
+
285
+ # Options that go along with each request
286
+ # service - :crs or :ship
287
+ def common_options(service)
288
+ {
289
+ :WebAuthenticationDetail => { :UserCredential => { :Key => @auth_key, :Password => @security_code } },
290
+ :ClientDetail => { :AccountNumber => @account_number, :MeterNumber => @meter_number },
291
+ :Version => WS_VERSION.merge({:ServiceId => service.to_s})
292
+ }
293
+ end
294
+
295
+ # Checks the supplied options for a given method or field and throws an exception if anything is missing
296
+ def check_required_options(option_set_name, options = {})
297
+ required_options = REQUIRED_OPTIONS[option_set_name]
298
+ missing = []
299
+ required_options.each{|option| missing << option if options[option].nil?}
300
+
301
+ unless missing.empty?
302
+ raise MissingInformationError.new("Missing #{missing.collect{|m| ":#{m}"}.join(', ')}")
303
+ end
304
+ end
305
+
306
+ # Creates and returns a driver for the requested action
307
+ def create_driver(name)
308
+ path = WSDL_PATHS[name]
309
+ unless path =~ /^\//
310
+ path = File.expand_path(DIR + '/' + WSDL_PATHS[name])
311
+ end
312
+ wsdl = SOAP::WSDLDriverFactory.new(path)
313
+ driver = wsdl.create_rpc_driver
314
+ # /s+(1000|0|9c9|fcc)\s+/ => ""
315
+ driver.wiredump_dev = STDOUT if @debug
316
+
317
+ driver
318
+ end
319
+
320
+ # Resolves the ground+residential discrepancy. If a package is shipped
321
+ # via Fedex Groundto an address marked as residential the service type must
322
+ # be set to ServiceTypes::GROUND_HOME_DELIVERY and not ServiceTypes::FEDEX_GROUND.
323
+ def resolve_service_type(service_type, residential)
324
+ if residential && (service_type == ServiceTypes::FEDEX_GROUND)
325
+ ServiceTypes::GROUND_HOME_DELIVERY
326
+ else
327
+ service_type
328
+ end
329
+ end
330
+
331
+ # Returns a boolean determining whether a request was successful.
332
+ def successful?(result)
333
+ if defined?(result.cancelPackageReply)
334
+ SUCCESSFUL_RESPONSES.any? {|r| r == result.cancelPackageReply.highestSeverity }
335
+ else
336
+ SUCCESSFUL_RESPONSES.any? {|r| r == result.highestSeverity }
337
+ end
338
+ end
339
+
340
+ # Returns the error message contained in the SOAP response, if one exists.
341
+ def error_msg(result, return_nothing_if_successful=true)
342
+ return "" if successful?(result) && return_nothing_if_successful
343
+ notes = result.notifications
344
+ notes.respond_to?(:message) ? notes.message : notes.first.message
345
+ end
346
+
347
+ # Attempts to determine the carrier code for a tracking number based upon its length.
348
+ # Currently supports Fedex Ground and Fedex Express
349
+ def carrier_code_for_tracking_number(tracking_number)
350
+ case tracking_number.length
351
+ when 12
352
+ 'FDXE'
353
+ when 15
354
+ 'FDXG'
355
+ end
356
+ end
357
+
358
+ def build_shipment_options(service, options)
359
+ # Prepare variables
360
+ order_number = options[:order_number] || ''
361
+
362
+ shipper = options[:shipper]
363
+ recipient = options[:recipient]
364
+
365
+ shipper_contact = shipper[:contact]
366
+ shipper_address = shipper[:address]
367
+
368
+ recipient_contact = recipient[:contact]
369
+ recipient_address = recipient[:address]
370
+
371
+ count = options[:count] || 1
372
+ weight = options[:weight]
373
+
374
+ time = options[:time] || Time.now
375
+ time = time.to_time.iso8601 if time.is_a?(Time)
376
+
377
+ residential = !!recipient_address[:residential]
378
+
379
+ service_type = options[:service_type]
380
+ service_type = resolve_service_type(service_type, residential) if service_type
381
+
382
+ common_options(service||:crs).merge(
383
+ :RequestedShipment => {
384
+ :Shipper => {
385
+ :Contact => {
386
+ :PersonName => shipper_contact[:name],
387
+ :PhoneNumber => shipper_contact[:phone_number]
388
+ },
389
+ :Address => {
390
+ :CountryCode => shipper_address[:country],
391
+ :StreetLines => shipper_address[:street],
392
+ :City => shipper_address[:city],
393
+ :StateOrProvinceCode => shipper_address[:state],
394
+ :PostalCode => shipper_address[:zip]
395
+ }
396
+ },
397
+ :Recipient => {
398
+ :Contact => {
399
+ :PersonName => recipient_contact[:name],
400
+ :PhoneNumber => recipient_contact[:phone_number]
401
+ },
402
+ :Address => {
403
+ :CountryCode => recipient_address[:country],
404
+ :StreetLines => recipient_address[:street],
405
+ :City => recipient_address[:city],
406
+ :StateOrProvinceCode => recipient_address[:state],
407
+ :PostalCode => recipient_address[:zip],
408
+ :Residential => residential
409
+ }
410
+ },
411
+ :ShippingChargesPayment => {
412
+ :PaymentType => @payment_type,
413
+ :Payor => {
414
+ :AccountNumber => @account_number,
415
+ :CountryCode => shipper_address[:country]
416
+ }
417
+ },
418
+ :LabelSpecification => {
419
+ :LabelFormatType => @label_type,
420
+ :ImageType => @label_image_type
421
+ },
422
+ :RateRequestTypes => @rate_request_type,
423
+ :PackageCount => count,
424
+ :ShipTimestamp => time,
425
+ :DropoffType => @dropoff_type,
426
+ :ServiceType => service_type,
427
+ :PackagingType => @packaging_type,
428
+ :PackageDetail => RequestedPackageDetailTypes::INDIVIDUAL_PACKAGES,
429
+ :PackageDetailSpecified => true,
430
+ :TotalWeight => { :Units => @units, :Value => weight },
431
+ :PreferredCurrency => @currency,
432
+ :RequestedPackageLineItems => package_line_items(options)
433
+ }
434
+ )
435
+ end
436
+
437
+ def package_line_items(options)
438
+ line_items = {
439
+ :SequenceNumber => 1,
440
+ :Weight => {
441
+ :Units => @units,
442
+ :Value => options[:weight]
443
+ },
444
+ :SpecialServicesRequested => {
445
+ :SpecialServiceTypes => []
446
+ }
447
+ }
448
+
449
+ if options[:dry_ice]
450
+ dry_ice_type = options[:dry_ice_type] || PackageSpecialServiceTypes::DRY_ICE
451
+ line_items[:SpecialServicesRequested][:SpecialServiceTypes] << dry_ice_type
452
+
453
+ line_items[:SpecialServicesRequested].merge!(
454
+ :DryIceWeight => {
455
+ :Units => options[:dry_ice_weight_units] || WeightUnits::KG,
456
+ :Value => options[:dry_ice_weight]
457
+ }
458
+ )
459
+ end
460
+ if options[:dangerous_goods]
461
+ dangerous_goods_type = options[:dangerous_goods_type] || PackageSpecialServiceTypes::DANGEROUS_GOODS
462
+ line_items[:SpecialServicesRequested][:SpecialServiceTypes] << dangerous_goods_type
463
+
464
+ line_items[:SpecialServicesRequested].merge!(
465
+ :DangerousGoodsDetail => {
466
+ :Accessibility => options[:dangerous_goods_accessibility] || DangerousGoodsAccessibilityTypes::INACCESSIBLE
467
+ }
468
+ )
469
+ end
470
+
471
+ [line_items]
472
+ end
473
+
474
+ end
475
+ end