active_shipping 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|