copious-fedex 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +6 -0
- data/MIT-LICENSE +19 -0
- data/README.rdoc +157 -0
- data/Rakefile +37 -0
- data/VERSION +1 -0
- data/lib/fedex.rb +475 -0
- data/lib/fedex/rate_constants.rb +608 -0
- data/lib/fedex/ship_constants.rb +678 -0
- data/lib/soap/property +4 -0
- data/rails/init.rb +27 -0
- data/test/.gitignore +1 -0
- data/test/integration_test.rb +11 -0
- data/test/integration_test_runner.rb +73 -0
- data/tmp/.gitignore +2 -0
- metadata +70 -0
data/.gitignore
ADDED
data/MIT-LICENSE
ADDED
@@ -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.
|
data/README.rdoc
ADDED
@@ -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.
|
data/Rakefile
ADDED
@@ -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
|
data/lib/fedex.rb
ADDED
@@ -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
|