active_shipping 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +6 -0
- data/CHANGELOG +23 -0
- data/MIT-LICENSE +20 -0
- data/README.markdown +173 -0
- data/Rakefile +55 -0
- data/VERSION +1 -0
- data/init.rb +1 -0
- data/lib/active_shipping.rb +50 -0
- data/lib/active_shipping/lib/connection.rb +170 -0
- data/lib/active_shipping/lib/country.rb +319 -0
- data/lib/active_shipping/lib/error.rb +4 -0
- data/lib/active_shipping/lib/post_data.rb +22 -0
- data/lib/active_shipping/lib/posts_data.rb +47 -0
- data/lib/active_shipping/lib/requires_parameters.rb +16 -0
- data/lib/active_shipping/lib/utils.rb +18 -0
- data/lib/active_shipping/lib/validateable.rb +76 -0
- data/lib/active_shipping/shipping/base.rb +15 -0
- data/lib/active_shipping/shipping/carrier.rb +75 -0
- data/lib/active_shipping/shipping/carriers.rb +17 -0
- data/lib/active_shipping/shipping/carriers/bogus_carrier.rb +16 -0
- data/lib/active_shipping/shipping/carriers/fedex.rb +315 -0
- data/lib/active_shipping/shipping/carriers/shipwire.rb +167 -0
- data/lib/active_shipping/shipping/carriers/ups.rb +368 -0
- data/lib/active_shipping/shipping/carriers/usps.rb +496 -0
- data/lib/active_shipping/shipping/location.rb +100 -0
- data/lib/active_shipping/shipping/package.rb +144 -0
- data/lib/active_shipping/shipping/rate_estimate.rb +54 -0
- data/lib/active_shipping/shipping/rate_response.rb +19 -0
- data/lib/active_shipping/shipping/response.rb +49 -0
- data/lib/active_shipping/shipping/shipment_event.rb +14 -0
- data/lib/active_shipping/shipping/tracking_response.rb +22 -0
- data/lib/certs/cacert.pem +7815 -0
- data/lib/vendor/quantified/MIT-LICENSE +22 -0
- data/lib/vendor/quantified/README.markdown +49 -0
- data/lib/vendor/quantified/Rakefile +21 -0
- data/lib/vendor/quantified/init.rb +0 -0
- data/lib/vendor/quantified/lib/quantified.rb +6 -0
- data/lib/vendor/quantified/lib/quantified/attribute.rb +208 -0
- data/lib/vendor/quantified/lib/quantified/length.rb +20 -0
- data/lib/vendor/quantified/lib/quantified/mass.rb +19 -0
- data/lib/vendor/quantified/test/length_test.rb +92 -0
- data/lib/vendor/quantified/test/mass_test.rb +88 -0
- data/lib/vendor/quantified/test/test_helper.rb +2 -0
- data/lib/vendor/test_helper.rb +13 -0
- data/lib/vendor/xml_node/README +36 -0
- data/lib/vendor/xml_node/Rakefile +21 -0
- data/lib/vendor/xml_node/benchmark/bench_generation.rb +32 -0
- data/lib/vendor/xml_node/init.rb +1 -0
- data/lib/vendor/xml_node/lib/xml_node.rb +222 -0
- data/lib/vendor/xml_node/test/test_generating.rb +94 -0
- data/lib/vendor/xml_node/test/test_parsing.rb +43 -0
- data/test/fixtures.yml +13 -0
- data/test/fixtures/xml/fedex/ottawa_to_beverly_hills_rate_request.xml +67 -0
- data/test/fixtures/xml/fedex/ottawa_to_beverly_hills_rate_response.xml +213 -0
- data/test/fixtures/xml/fedex/tracking_request.xml +27 -0
- data/test/fixtures/xml/fedex/tracking_response.xml +153 -0
- data/test/fixtures/xml/shipwire/international_rates_response.xml +17 -0
- data/test/fixtures/xml/shipwire/invalid_credentials_response.xml +4 -0
- data/test/fixtures/xml/shipwire/new_carrier_rate_response.xml +18 -0
- data/test/fixtures/xml/shipwire/no_rates_response.xml +7 -0
- data/test/fixtures/xml/shipwire/rates_response.xml +36 -0
- data/test/fixtures/xml/ups/example_tracking_response.xml +53 -0
- data/test/fixtures/xml/ups/shipment_from_tiger_direct.xml +222 -0
- data/test/fixtures/xml/ups/test_real_home_as_residential_destination_response.xml +1 -0
- data/test/fixtures/xml/usps/beverly_hills_to_ottawa_book_rate_response.xml +85 -0
- data/test/fixtures/xml/usps/beverly_hills_to_ottawa_book_wii_rate_response.xml +168 -0
- data/test/fixtures/xml/usps/beverly_hills_to_ottawa_wii_rate_response.xml +85 -0
- data/test/remote/fedex_test.rb +140 -0
- data/test/remote/shipwire_test.rb +88 -0
- data/test/remote/ups_test.rb +187 -0
- data/test/remote/usps_test.rb +184 -0
- data/test/test_helper.rb +167 -0
- data/test/unit/base_test.rb +18 -0
- data/test/unit/carriers/fedex_test.rb +78 -0
- data/test/unit/carriers/shipwire_test.rb +130 -0
- data/test/unit/carriers/ups_test.rb +81 -0
- data/test/unit/carriers/usps_test.rb +206 -0
- data/test/unit/location_test.rb +46 -0
- data/test/unit/package_test.rb +65 -0
- data/test/unit/response_test.rb +10 -0
- metadata +158 -0
@@ -0,0 +1,15 @@
|
|
1
|
+
module ActiveMerchant
|
2
|
+
module Shipping
|
3
|
+
module Base
|
4
|
+
mattr_accessor :mode
|
5
|
+
self.mode = :production
|
6
|
+
|
7
|
+
ALLCAPS_NAMES = ['ups','usps','dhl'] # is the class name allcaps like USPS or camelcase like FedEx?
|
8
|
+
|
9
|
+
def self.carrier(name)
|
10
|
+
name = name.to_s.downcase
|
11
|
+
ActiveMerchant::Shipping.const_get(ALLCAPS_NAMES.include?(name) ? name.upcase : name.camelize)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
module ActiveMerchant
|
2
|
+
module Shipping
|
3
|
+
class Carrier
|
4
|
+
|
5
|
+
include RequiresParameters
|
6
|
+
include PostsData
|
7
|
+
include Quantified
|
8
|
+
|
9
|
+
attr_reader :last_request
|
10
|
+
attr_accessor :test_mode
|
11
|
+
alias_method :test_mode?, :test_mode
|
12
|
+
|
13
|
+
# Credentials should be in options hash under keys :login, :password and/or :key.
|
14
|
+
def initialize(options = {})
|
15
|
+
requirements.each {|key| requires!(options, key)}
|
16
|
+
@options = options
|
17
|
+
@last_request = nil
|
18
|
+
@test_mode = @options[:test]
|
19
|
+
end
|
20
|
+
|
21
|
+
# Override to return required keys in options hash for initialize method.
|
22
|
+
def requirements
|
23
|
+
[]
|
24
|
+
end
|
25
|
+
|
26
|
+
# Override with whatever you need to get the rates
|
27
|
+
def find_rates(origin, destination, packages, options = {})
|
28
|
+
end
|
29
|
+
|
30
|
+
# Validate credentials with a call to the API. By default this just does a find_rates call
|
31
|
+
# with the orgin and destination both as the carrier's default_location. Override to provide
|
32
|
+
# alternate functionality, such as checking for test_mode to use test servers, etc.
|
33
|
+
def valid_credentials?
|
34
|
+
location = self.class.default_location
|
35
|
+
find_rates(location,location,Package.new(100, [5,15,30]))
|
36
|
+
rescue ActiveMerchant::Shipping::ResponseError
|
37
|
+
false
|
38
|
+
else
|
39
|
+
true
|
40
|
+
end
|
41
|
+
|
42
|
+
def maximum_weight
|
43
|
+
Mass.new(150, :pounds)
|
44
|
+
end
|
45
|
+
|
46
|
+
protected
|
47
|
+
|
48
|
+
def node_string_or_nil(xml_node)
|
49
|
+
text = node_text_or_nil(xml_node)
|
50
|
+
text ? text.to_s : nil
|
51
|
+
end
|
52
|
+
|
53
|
+
def node_text_or_nil(xml_node)
|
54
|
+
xml_node ? xml_node.text : nil
|
55
|
+
end
|
56
|
+
|
57
|
+
# Override in subclasses for non-U.S.-based carriers.
|
58
|
+
def self.default_location
|
59
|
+
Location.new( :country => 'US',
|
60
|
+
:state => 'CA',
|
61
|
+
:city => 'Beverly Hills',
|
62
|
+
:address1 => '455 N. Rexford Dr.',
|
63
|
+
:address2 => '3rd Floor',
|
64
|
+
:zip => '90210',
|
65
|
+
:phone => '1-310-285-1013',
|
66
|
+
:fax => '1-310-275-8159')
|
67
|
+
end
|
68
|
+
|
69
|
+
# Use after building the request to save for later inspection. Probably won't ever be overridden.
|
70
|
+
def save_request(r)
|
71
|
+
@last_request = r
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'active_shipping/shipping/carriers/bogus_carrier'
|
2
|
+
require 'active_shipping/shipping/carriers/ups'
|
3
|
+
require 'active_shipping/shipping/carriers/usps'
|
4
|
+
require 'active_shipping/shipping/carriers/fedex'
|
5
|
+
require 'active_shipping/shipping/carriers/shipwire'
|
6
|
+
|
7
|
+
module ActiveMerchant
|
8
|
+
module Shipping
|
9
|
+
module Carriers
|
10
|
+
class <<self
|
11
|
+
def all
|
12
|
+
[BogusCarrier, UPS, USPS, FedEx]
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module ActiveMerchant
|
2
|
+
module Shipping
|
3
|
+
class BogusCarrier < Carrier
|
4
|
+
cattr_reader :name
|
5
|
+
@@name = "Bogus Carrier"
|
6
|
+
|
7
|
+
|
8
|
+
def find_rates(origin, destination, packages, options = {})
|
9
|
+
origin = Location.from(origin)
|
10
|
+
destination = Location.from(destination)
|
11
|
+
packages = Array(packages)
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,315 @@
|
|
1
|
+
# FedEx module by Jimmy Baker
|
2
|
+
# http://github.com/jimmyebaker
|
3
|
+
|
4
|
+
module ActiveMerchant
|
5
|
+
module Shipping
|
6
|
+
|
7
|
+
# :key is your developer API key
|
8
|
+
# :password is your API password
|
9
|
+
# :account is your FedEx account number
|
10
|
+
# :login is your meter number
|
11
|
+
class FedEx < Carrier
|
12
|
+
self.retry_safe = true
|
13
|
+
|
14
|
+
cattr_reader :name
|
15
|
+
@@name = "FedEx"
|
16
|
+
|
17
|
+
TEST_URL = 'https://gatewaybeta.fedex.com:443/xml'
|
18
|
+
LIVE_URL = 'https://gateway.fedex.com:443/xml'
|
19
|
+
|
20
|
+
CarrierCodes = {
|
21
|
+
"fedex_ground" => "FDXG",
|
22
|
+
"fedex_express" => "FDXE"
|
23
|
+
}
|
24
|
+
|
25
|
+
ServiceTypes = {
|
26
|
+
"PRIORITY_OVERNIGHT" => "FedEx Priority Overnight",
|
27
|
+
"PRIORITY_OVERNIGHT_SATURDAY_DELIVERY" => "FedEx Priority Overnight Saturday Delivery",
|
28
|
+
"FEDEX_2_DAY" => "FedEx 2 Day",
|
29
|
+
"FEDEX_2_DAY_SATURDAY_DELIVERY" => "FedEx 2 Day Saturday Delivery",
|
30
|
+
"STANDARD_OVERNIGHT" => "FedEx Standard Overnight",
|
31
|
+
"FIRST_OVERNIGHT" => "FedEx First Overnight",
|
32
|
+
"FEDEX_EXPRESS_SAVER" => "FedEx Express Saver",
|
33
|
+
"FEDEX_1_DAY_FREIGHT" => "FedEx 1 Day Freight",
|
34
|
+
"FEDEX_1_DAY_FREIGHT_SATURDAY_DELIVERY" => "FedEx 1 Day Freight Saturday Delivery",
|
35
|
+
"FEDEX_2_DAY_FREIGHT" => "FedEx 2 Day Freight",
|
36
|
+
"FEDEX_2_DAY_FREIGHT_SATURDAY_DELIVERY" => "FedEx 2 Day Freight Saturday Delivery",
|
37
|
+
"FEDEX_3_DAY_FREIGHT" => "FedEx 3 Day Freight",
|
38
|
+
"FEDEX_3_DAY_FREIGHT_SATURDAY_DELIVERY" => "FedEx 3 Day Freight Saturday Delivery",
|
39
|
+
"INTERNATIONAL_PRIORITY" => "FedEx International Priority",
|
40
|
+
"INTERNATIONAL_PRIORITY_SATURDAY_DELIVERY" => "FedEx International Priority Saturday Delivery",
|
41
|
+
"INTERNATIONAL_ECONOMY" => "FedEx International Economy",
|
42
|
+
"INTERNATIONAL_FIRST" => "FedEx International First",
|
43
|
+
"INTERNATIONAL_PRIORITY_FREIGHT" => "FedEx International Priority Freight",
|
44
|
+
"INTERNATIONAL_ECONOMY_FREIGHT" => "FedEx International Economy Freight",
|
45
|
+
"GROUND_HOME_DELIVERY" => "FedEx Ground Home Delivery",
|
46
|
+
"FEDEX_GROUND" => "FedEx Ground",
|
47
|
+
"INTERNATIONAL_GROUND" => "FedEx International Ground"
|
48
|
+
}
|
49
|
+
|
50
|
+
PackageTypes = {
|
51
|
+
"fedex_envelope" => "FEDEX_ENVELOPE",
|
52
|
+
"fedex_pak" => "FEDEX_PAK",
|
53
|
+
"fedex_box" => "FEDEX_BOX",
|
54
|
+
"fedex_tube" => "FEDEX_TUBE",
|
55
|
+
"fedex_10_kg_box" => "FEDEX_10KG_BOX",
|
56
|
+
"fedex_25_kg_box" => "FEDEX_25KG_BOX",
|
57
|
+
"your_packaging" => "YOUR_PACKAGING"
|
58
|
+
}
|
59
|
+
|
60
|
+
DropoffTypes = {
|
61
|
+
'regular_pickup' => 'REGULAR_PICKUP',
|
62
|
+
'request_courier' => 'REQUEST_COURIER',
|
63
|
+
'dropbox' => 'DROP_BOX',
|
64
|
+
'business_service_center' => 'BUSINESS_SERVICE_CENTER',
|
65
|
+
'station' => 'STATION'
|
66
|
+
}
|
67
|
+
|
68
|
+
PaymentTypes = {
|
69
|
+
'sender' => 'SENDER',
|
70
|
+
'recipient' => 'RECIPIENT',
|
71
|
+
'third_party' => 'THIRDPARTY',
|
72
|
+
'collect' => 'COLLECT'
|
73
|
+
}
|
74
|
+
|
75
|
+
PackageIdentifierTypes = {
|
76
|
+
'tracking_number' => 'TRACKING_NUMBER_OR_DOORTAG',
|
77
|
+
'door_tag' => 'TRACKING_NUMBER_OR_DOORTAG',
|
78
|
+
'rma' => 'RMA',
|
79
|
+
'ground_shipment_id' => 'GROUND_SHIPMENT_ID',
|
80
|
+
'ground_invoice_number' => 'GROUND_INVOICE_NUMBER',
|
81
|
+
'ground_customer_reference' => 'GROUND_CUSTOMER_REFERENCE',
|
82
|
+
'ground_po' => 'GROUND_PO',
|
83
|
+
'express_reference' => 'EXPRESS_REFERENCE',
|
84
|
+
'express_mps_master' => 'EXPRESS_MPS_MASTER'
|
85
|
+
}
|
86
|
+
|
87
|
+
def requirements
|
88
|
+
[:key, :password, :account, :login]
|
89
|
+
end
|
90
|
+
|
91
|
+
def find_rates(origin, destination, packages, options = {})
|
92
|
+
options = @options.update(options)
|
93
|
+
packages = Array(packages)
|
94
|
+
|
95
|
+
rate_request = build_rate_request(origin, destination, packages, options)
|
96
|
+
|
97
|
+
response = commit(save_request(rate_request), (options[:test] || false)).gsub(/<(\/)?.*?\:(.*?)>/, '<\1\2>')
|
98
|
+
|
99
|
+
parse_rate_response(origin, destination, packages, response, options)
|
100
|
+
end
|
101
|
+
|
102
|
+
def find_tracking_info(tracking_number, options={})
|
103
|
+
options = @options.update(options)
|
104
|
+
|
105
|
+
tracking_request = build_tracking_request(tracking_number, options)
|
106
|
+
response = commit(save_request(tracking_request), (options[:test] || false)).gsub(/<(\/)?.*?\:(.*?)>/, '<\1\2>')
|
107
|
+
parse_tracking_response(response, options)
|
108
|
+
end
|
109
|
+
|
110
|
+
protected
|
111
|
+
def build_rate_request(origin, destination, packages, options={})
|
112
|
+
imperial = ['US','LR','MM'].include?(origin.country_code(:alpha2))
|
113
|
+
|
114
|
+
xml_request = XmlNode.new('RateRequest', 'xmlns' => 'http://fedex.com/ws/rate/v6') do |root_node|
|
115
|
+
root_node << build_request_header
|
116
|
+
|
117
|
+
# Version
|
118
|
+
root_node << XmlNode.new('Version') do |version_node|
|
119
|
+
version_node << XmlNode.new('ServiceId', 'crs')
|
120
|
+
version_node << XmlNode.new('Major', '6')
|
121
|
+
version_node << XmlNode.new('Intermediate', '0')
|
122
|
+
version_node << XmlNode.new('Minor', '0')
|
123
|
+
end
|
124
|
+
|
125
|
+
# Returns delivery dates
|
126
|
+
root_node << XmlNode.new('ReturnTransitAndCommit', true)
|
127
|
+
# Returns saturday delivery shipping options when available
|
128
|
+
root_node << XmlNode.new('VariableOptions', 'SATURDAY_DELIVERY')
|
129
|
+
|
130
|
+
root_node << XmlNode.new('RequestedShipment') do |rs|
|
131
|
+
rs << XmlNode.new('ShipTimestamp', Time.now)
|
132
|
+
rs << XmlNode.new('DropoffType', options[:dropoff_type] || 'REGULAR_PICKUP')
|
133
|
+
rs << XmlNode.new('PackagingType', options[:packaging_type] || 'YOUR_PACKAGING')
|
134
|
+
|
135
|
+
rs << build_location_node('Shipper', (options[:shipper] || origin))
|
136
|
+
rs << build_location_node('Recipient', destination)
|
137
|
+
if options[:shipper] and options[:shipper] != origin
|
138
|
+
rs << build_location_node('Origin', origin)
|
139
|
+
end
|
140
|
+
|
141
|
+
rs << XmlNode.new('RateRequestTypes', 'ACCOUNT')
|
142
|
+
rs << XmlNode.new('PackageCount', packages.size)
|
143
|
+
packages.each do |pkg|
|
144
|
+
rs << XmlNode.new('RequestedPackages') do |rps|
|
145
|
+
rps << XmlNode.new('Weight') do |tw|
|
146
|
+
tw << XmlNode.new('Units', imperial ? 'LB' : 'KG')
|
147
|
+
tw << XmlNode.new('Value', [((imperial ? pkg.lbs : pkg.kgs).to_f*1000).round/1000.0, 0.1].max)
|
148
|
+
end
|
149
|
+
rps << XmlNode.new('Dimensions') do |dimensions|
|
150
|
+
[:length,:width,:height].each do |axis|
|
151
|
+
value = ((imperial ? pkg.inches(axis) : pkg.cm(axis)).to_f*1000).round/1000.0 # 3 decimals
|
152
|
+
dimensions << XmlNode.new(axis.to_s.capitalize, value.ceil)
|
153
|
+
end
|
154
|
+
dimensions << XmlNode.new('Units', imperial ? 'IN' : 'CM')
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
end
|
160
|
+
end
|
161
|
+
xml_request.to_s
|
162
|
+
end
|
163
|
+
|
164
|
+
def build_tracking_request(tracking_number, options={})
|
165
|
+
xml_request = XmlNode.new('TrackRequest', 'xmlns' => 'http://fedex.com/ws/track/v3') do |root_node|
|
166
|
+
root_node << build_request_header
|
167
|
+
|
168
|
+
# Version
|
169
|
+
root_node << XmlNode.new('Version') do |version_node|
|
170
|
+
version_node << XmlNode.new('ServiceId', 'trck')
|
171
|
+
version_node << XmlNode.new('Major', '3')
|
172
|
+
version_node << XmlNode.new('Intermediate', '0')
|
173
|
+
version_node << XmlNode.new('Minor', '0')
|
174
|
+
end
|
175
|
+
|
176
|
+
root_node << XmlNode.new('PackageIdentifier') do |package_node|
|
177
|
+
package_node << XmlNode.new('Value', tracking_number)
|
178
|
+
package_node << XmlNode.new('Type', PackageIdentifierTypes[options['package_identifier_type'] || 'tracking_number'])
|
179
|
+
end
|
180
|
+
|
181
|
+
root_node << XmlNode.new('ShipDateRangeBegin', options['ship_date_range_begin']) if options['ship_date_range_begin']
|
182
|
+
root_node << XmlNode.new('ShipDateRangeEnd', options['ship_date_range_end']) if options['ship_date_range_end']
|
183
|
+
root_node << XmlNode.new('IncludeDetailedScans', 1)
|
184
|
+
end
|
185
|
+
xml_request.to_s
|
186
|
+
end
|
187
|
+
|
188
|
+
def build_request_header
|
189
|
+
web_authentication_detail = XmlNode.new('WebAuthenticationDetail') do |wad|
|
190
|
+
wad << XmlNode.new('UserCredential') do |uc|
|
191
|
+
uc << XmlNode.new('Key', @options[:key])
|
192
|
+
uc << XmlNode.new('Password', @options[:password])
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
client_detail = XmlNode.new('ClientDetail') do |cd|
|
197
|
+
cd << XmlNode.new('AccountNumber', @options[:account])
|
198
|
+
cd << XmlNode.new('MeterNumber', @options[:login])
|
199
|
+
end
|
200
|
+
|
201
|
+
trasaction_detail = XmlNode.new('TransactionDetail') do |td|
|
202
|
+
td << XmlNode.new('CustomerTransactionId', 'ActiveShipping') # TODO: Need to do something better with this..
|
203
|
+
end
|
204
|
+
|
205
|
+
[web_authentication_detail, client_detail, trasaction_detail]
|
206
|
+
end
|
207
|
+
|
208
|
+
def build_location_node(name, location)
|
209
|
+
location_node = XmlNode.new(name) do |xml_node|
|
210
|
+
xml_node << XmlNode.new('Address') do |address_node|
|
211
|
+
address_node << XmlNode.new('PostalCode', location.postal_code)
|
212
|
+
address_node << XmlNode.new("CountryCode", location.country_code(:alpha2))
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
def parse_rate_response(origin, destination, packages, response, options)
|
218
|
+
rate_estimates = []
|
219
|
+
success, message = nil
|
220
|
+
|
221
|
+
xml = REXML::Document.new(response)
|
222
|
+
root_node = xml.elements['RateReply']
|
223
|
+
|
224
|
+
success = response_success?(xml)
|
225
|
+
message = response_message(xml)
|
226
|
+
|
227
|
+
root_node.elements.each('RateReplyDetails') do |rated_shipment|
|
228
|
+
service_code = rated_shipment.get_text('ServiceType').to_s
|
229
|
+
is_saturday_delivery = rated_shipment.get_text('AppliedOptions').to_s == 'SATURDAY_DELIVERY'
|
230
|
+
service_type = is_saturday_delivery ? "#{rated_shipment.get_text('ServiceType').to_s}_SATURDAY_DELIVERY" : rated_shipment.get_text('ServiceType').to_s
|
231
|
+
|
232
|
+
rate_estimates << RateEstimate.new(origin, destination, @@name,
|
233
|
+
ServiceTypes[service_type],
|
234
|
+
:service_code => service_code,
|
235
|
+
:total_price => rated_shipment.get_text('RatedShipmentDetails/ShipmentRateDetail/TotalNetCharge/Amount').to_s.to_f,
|
236
|
+
:currency => rated_shipment.get_text('RatedShipmentDetails/ShipmentRateDetail/TotalNetCharge/Currency').to_s,
|
237
|
+
:packages => packages,
|
238
|
+
:delivery_date => rated_shipment.get_text('DeliveryTimestamp').to_s)
|
239
|
+
end
|
240
|
+
|
241
|
+
if rate_estimates.empty?
|
242
|
+
success = false
|
243
|
+
message = "No shipping rates could be found for the destination address" if message.blank?
|
244
|
+
end
|
245
|
+
|
246
|
+
RateResponse.new(success, message, Hash.from_xml(response), :rates => rate_estimates, :xml => response, :request => last_request, :log_xml => options[:log_xml])
|
247
|
+
end
|
248
|
+
|
249
|
+
def parse_tracking_response(response, options)
|
250
|
+
xml = REXML::Document.new(response)
|
251
|
+
root_node = xml.elements['TrackReply']
|
252
|
+
|
253
|
+
success = response_success?(xml)
|
254
|
+
message = response_message(xml)
|
255
|
+
|
256
|
+
if success
|
257
|
+
tracking_number, origin, destination = nil
|
258
|
+
shipment_events = []
|
259
|
+
|
260
|
+
tracking_details = root_node.elements['TrackDetails']
|
261
|
+
tracking_number = tracking_details.get_text('TrackingNumber').to_s
|
262
|
+
|
263
|
+
destination_node = tracking_details.elements['DestinationAddress']
|
264
|
+
destination = Location.new(
|
265
|
+
:country => destination_node.get_text('CountryCode').to_s,
|
266
|
+
:province => destination_node.get_text('StateOrProvinceCode').to_s,
|
267
|
+
:city => destination_node.get_text('City').to_s
|
268
|
+
)
|
269
|
+
|
270
|
+
tracking_details.elements.each('Events') do |event|
|
271
|
+
location = Location.new(
|
272
|
+
:city => event.elements['Address'].get_text('City').to_s,
|
273
|
+
:state => event.elements['Address'].get_text('StateOrProvinceCode').to_s,
|
274
|
+
:postal_code => event.elements['Address'].get_text('PostalCode').to_s,
|
275
|
+
:country => event.elements['Address'].get_text('CountryCode').to_s)
|
276
|
+
description = event.get_text('EventDescription').to_s
|
277
|
+
|
278
|
+
# for now, just assume UTC, even though it probably isn't
|
279
|
+
time = Time.parse("#{event.get_text('Timestamp').to_s}")
|
280
|
+
zoneless_time = Time.utc(time.year, time.month, time.mday, time.hour, time.min, time.sec)
|
281
|
+
|
282
|
+
shipment_events << ShipmentEvent.new(description, zoneless_time, location)
|
283
|
+
end
|
284
|
+
shipment_events = shipment_events.sort_by(&:time)
|
285
|
+
end
|
286
|
+
|
287
|
+
TrackingResponse.new(success, message, Hash.from_xml(response),
|
288
|
+
:xml => response,
|
289
|
+
:request => last_request,
|
290
|
+
:shipment_events => shipment_events,
|
291
|
+
:destination => destination,
|
292
|
+
:tracking_number => tracking_number
|
293
|
+
)
|
294
|
+
end
|
295
|
+
|
296
|
+
def response_status_node(document)
|
297
|
+
document.elements['/*/Notifications/']
|
298
|
+
end
|
299
|
+
|
300
|
+
def response_success?(document)
|
301
|
+
%w{SUCCESS WARNING NOTE}.include? response_status_node(document).get_text('Severity').to_s
|
302
|
+
end
|
303
|
+
|
304
|
+
def response_message(document)
|
305
|
+
response_node = response_status_node(document)
|
306
|
+
"#{response_status_node(document).get_text('Severity').to_s} - #{response_node.get_text('Code').to_s}: #{response_node.get_text('Message').to_s}"
|
307
|
+
end
|
308
|
+
|
309
|
+
def commit(request, test = false)
|
310
|
+
ssl_post(test ? TEST_URL : LIVE_URL, request.gsub("\n",''))
|
311
|
+
end
|
312
|
+
|
313
|
+
end
|
314
|
+
end
|
315
|
+
end
|
@@ -0,0 +1,167 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
require 'builder'
|
3
|
+
|
4
|
+
module ActiveMerchant
|
5
|
+
module Shipping
|
6
|
+
class Shipwire < Carrier
|
7
|
+
self.retry_safe = true
|
8
|
+
|
9
|
+
cattr_reader :name
|
10
|
+
@@name = "Shipwire"
|
11
|
+
|
12
|
+
URL = 'https://api.shipwire.com/exec/RateServices.php'
|
13
|
+
SCHEMA_URL = 'http://www.shipwire.com/exec/download/RateRequest.dtd'
|
14
|
+
WAREHOUSES = { 'CHI' => 'Chicago',
|
15
|
+
'LAX' => 'Los Angeles',
|
16
|
+
'REN' => 'Reno',
|
17
|
+
'VAN' => 'Vancouver',
|
18
|
+
'TOR' => 'Toronto',
|
19
|
+
'UK' => 'United Kingdom'
|
20
|
+
}
|
21
|
+
|
22
|
+
CARRIERS = [ "UPS", "USPS", "FedEx", "Royal Mail", "Parcelforce", "Pharos", "Eurotrux", "Canada Post", "DHL" ]
|
23
|
+
|
24
|
+
SUCCESS = "OK"
|
25
|
+
SUCCESS_MESSAGE = "Successfully received the shipping rates"
|
26
|
+
NO_RATES_MESSAGE = "No shipping rates could be found for the destination address"
|
27
|
+
REQUIRED_OPTIONS = [:login, :password].freeze
|
28
|
+
|
29
|
+
def find_rates(origin, destination, packages, options = {})
|
30
|
+
requires!(options, :items)
|
31
|
+
commit(origin, destination, options)
|
32
|
+
end
|
33
|
+
|
34
|
+
def valid_credentials?
|
35
|
+
location = self.class.default_location
|
36
|
+
find_rates(location, location, Package.new(100, [5,15,30]),
|
37
|
+
:items => [ { :sku => '', :quantity => 1 } ]
|
38
|
+
)
|
39
|
+
rescue ActiveMerchant::Shipping::ResponseError => e
|
40
|
+
e.message != "Could not verify e-mail/password combination"
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
def requirements
|
45
|
+
REQUIRED_OPTIONS
|
46
|
+
end
|
47
|
+
|
48
|
+
def build_request(destination, options)
|
49
|
+
xml = Builder::XmlMarkup.new
|
50
|
+
xml.instruct!
|
51
|
+
xml.declare! :DOCTYPE, :RateRequest, :SYSTEM, SCHEMA_URL
|
52
|
+
xml.tag! 'RateRequest' do
|
53
|
+
add_credentials(xml)
|
54
|
+
add_order(xml, destination, options)
|
55
|
+
end
|
56
|
+
xml.target!
|
57
|
+
end
|
58
|
+
|
59
|
+
def add_credentials(xml)
|
60
|
+
xml.tag! 'EmailAddress', @options[:login]
|
61
|
+
xml.tag! 'Password', @options[:password]
|
62
|
+
end
|
63
|
+
|
64
|
+
def add_order(xml, destination, options)
|
65
|
+
xml.tag! 'Order', :id => options[:order_id] do
|
66
|
+
xml.tag! 'Warehouse', options[:warehouse] || '00'
|
67
|
+
|
68
|
+
add_address(xml, destination)
|
69
|
+
Array(options[:items]).each_with_index do |line_item, index|
|
70
|
+
add_item(xml, line_item, index)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def add_address(xml, destination)
|
76
|
+
xml.tag! 'AddressInfo', :type => 'Ship' do
|
77
|
+
xml.tag! 'Address1', destination.address1
|
78
|
+
xml.tag! 'Address2', destination.address2 unless destination.address2.blank?
|
79
|
+
xml.tag! 'Address3', destination.address3 unless destination.address3.blank?
|
80
|
+
xml.tag! 'City', destination.city
|
81
|
+
xml.tag! 'State', destination.state unless destination.state.blank?
|
82
|
+
xml.tag! 'Country', destination.country_code
|
83
|
+
xml.tag! 'Zip', destination.zip unless destination.zip.blank?
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# Code is limited to 12 characters
|
88
|
+
def add_item(xml, item, index)
|
89
|
+
xml.tag! 'Item', :num => index do
|
90
|
+
xml.tag! 'Code', item[:sku]
|
91
|
+
xml.tag! 'Quantity', item[:quantity]
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def commit(origin, destination, options)
|
96
|
+
request = build_request(destination, options)
|
97
|
+
save_request(request)
|
98
|
+
|
99
|
+
response = parse( ssl_post(URL, "RateRequestXML=#{CGI.escape(request)}") )
|
100
|
+
|
101
|
+
RateResponse.new(response["success"], response["message"], response,
|
102
|
+
:xml => response,
|
103
|
+
:rates => build_rate_estimates(response, origin, destination),
|
104
|
+
:request => last_request
|
105
|
+
)
|
106
|
+
end
|
107
|
+
|
108
|
+
def build_rate_estimates(response, origin, destination)
|
109
|
+
response["rates"].collect do |quote|
|
110
|
+
RateEstimate.new(origin, destination, carrier_for(quote["service"]), quote["service"],
|
111
|
+
:service_code => quote["method"],
|
112
|
+
:total_price => quote["cost"],
|
113
|
+
:currency => quote["currency"]
|
114
|
+
)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def carrier_for(service)
|
119
|
+
CARRIERS.dup.find{ |carrier| service.to_s =~ /^#{carrier}/i } || service.to_s.split(" ").first
|
120
|
+
end
|
121
|
+
|
122
|
+
def parse(xml)
|
123
|
+
response = {}
|
124
|
+
response["rates"] = []
|
125
|
+
|
126
|
+
document = REXML::Document.new(xml)
|
127
|
+
|
128
|
+
response["status"] = parse_child_text(document.root, "Status")
|
129
|
+
|
130
|
+
document.root.elements.each("Order/Quotes/Quote") do |e|
|
131
|
+
rate = {}
|
132
|
+
rate["method"] = e.attributes["method"]
|
133
|
+
rate["warehouse"] = parse_child_text(e, "Warehouse")
|
134
|
+
rate["service"] = parse_child_text(e, "Service")
|
135
|
+
rate["cost"] = parse_child_text(e, "Cost")
|
136
|
+
rate["currency"] = parse_child_attribute(e, "Cost", "currency")
|
137
|
+
response["rates"] << rate
|
138
|
+
end
|
139
|
+
|
140
|
+
if response["status"] == SUCCESS && response["rates"].any?
|
141
|
+
response["success"] = true
|
142
|
+
response["message"] = SUCCESS_MESSAGE
|
143
|
+
elsif response["status"] == SUCCESS && response["rates"].empty?
|
144
|
+
response["success"] = false
|
145
|
+
response["message"] = NO_RATES_MESSAGE
|
146
|
+
else
|
147
|
+
response["success"] = false
|
148
|
+
response["message"] = parse_child_text(document.root, "ErrorMessage")
|
149
|
+
end
|
150
|
+
|
151
|
+
response
|
152
|
+
end
|
153
|
+
|
154
|
+
def parse_child_text(parent, name)
|
155
|
+
if element = parent.elements[name]
|
156
|
+
element.text
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def parse_child_attribute(parent, name, attribute)
|
161
|
+
if element = parent.elements[name]
|
162
|
+
element.attributes[attribute]
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|