active_shipping 0.12.6 → 1.0.0.pre1
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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +2 -0
- data.tar.gz.sig +0 -0
- data/{CHANGELOG → CHANGELOG.md} +6 -2
- data/CONTRIBUTING.md +32 -0
- data/{README.markdown → README.md} +45 -61
- data/lib/active_shipping.rb +20 -28
- data/lib/active_shipping/carrier.rb +82 -0
- data/lib/active_shipping/carriers.rb +33 -0
- data/lib/active_shipping/carriers/benchmark_carrier.rb +31 -0
- data/lib/active_shipping/carriers/bogus_carrier.rb +12 -0
- data/lib/active_shipping/carriers/canada_post.rb +253 -0
- data/lib/active_shipping/carriers/canada_post_pws.rb +870 -0
- data/lib/active_shipping/carriers/fedex.rb +579 -0
- data/lib/active_shipping/carriers/kunaki.rb +164 -0
- data/lib/active_shipping/carriers/new_zealand_post.rb +262 -0
- data/lib/active_shipping/carriers/shipwire.rb +181 -0
- data/lib/active_shipping/carriers/stamps.rb +861 -0
- data/lib/active_shipping/carriers/ups.rb +648 -0
- data/lib/active_shipping/carriers/usps.rb +642 -0
- data/lib/active_shipping/errors.rb +7 -0
- data/lib/active_shipping/label_response.rb +23 -0
- data/lib/active_shipping/location.rb +149 -0
- data/lib/active_shipping/package.rb +241 -0
- data/lib/active_shipping/rate_estimate.rb +64 -0
- data/lib/active_shipping/rate_response.rb +13 -0
- data/lib/active_shipping/response.rb +41 -0
- data/lib/active_shipping/shipment_event.rb +17 -0
- data/lib/active_shipping/shipment_packer.rb +73 -0
- data/lib/active_shipping/shipping_response.rb +12 -0
- data/lib/active_shipping/tracking_response.rb +52 -0
- data/lib/active_shipping/version.rb +1 -1
- data/lib/vendor/quantified/test/length_test.rb +2 -2
- data/lib/vendor/xml_node/test/test_parsing.rb +1 -1
- metadata +58 -36
- metadata.gz.sig +0 -0
- data/lib/active_shipping/shipping/base.rb +0 -13
- data/lib/active_shipping/shipping/carrier.rb +0 -84
- data/lib/active_shipping/shipping/carriers.rb +0 -23
- data/lib/active_shipping/shipping/carriers/benchmark_carrier.rb +0 -33
- data/lib/active_shipping/shipping/carriers/bogus_carrier.rb +0 -14
- data/lib/active_shipping/shipping/carriers/canada_post.rb +0 -257
- data/lib/active_shipping/shipping/carriers/canada_post_pws.rb +0 -874
- data/lib/active_shipping/shipping/carriers/fedex.rb +0 -581
- data/lib/active_shipping/shipping/carriers/kunaki.rb +0 -166
- data/lib/active_shipping/shipping/carriers/new_zealand_post.rb +0 -262
- data/lib/active_shipping/shipping/carriers/shipwire.rb +0 -184
- data/lib/active_shipping/shipping/carriers/stamps.rb +0 -864
- data/lib/active_shipping/shipping/carriers/ups.rb +0 -650
- data/lib/active_shipping/shipping/carriers/usps.rb +0 -649
- data/lib/active_shipping/shipping/errors.rb +0 -9
- data/lib/active_shipping/shipping/label_response.rb +0 -25
- data/lib/active_shipping/shipping/location.rb +0 -152
- data/lib/active_shipping/shipping/package.rb +0 -243
- data/lib/active_shipping/shipping/rate_estimate.rb +0 -66
- data/lib/active_shipping/shipping/rate_response.rb +0 -15
- data/lib/active_shipping/shipping/response.rb +0 -43
- data/lib/active_shipping/shipping/shipment_event.rb +0 -19
- data/lib/active_shipping/shipping/shipment_packer.rb +0 -75
- data/lib/active_shipping/shipping/shipping_response.rb +0 -14
- data/lib/active_shipping/shipping/tracking_response.rb +0 -54
@@ -1,649 +0,0 @@
|
|
1
|
-
# -*- encoding: utf-8 -*-
|
2
|
-
require 'cgi'
|
3
|
-
|
4
|
-
module ActiveMerchant
|
5
|
-
module Shipping
|
6
|
-
# After getting an API login from USPS (looks like '123YOURNAME456'),
|
7
|
-
# run the following test:
|
8
|
-
#
|
9
|
-
# usps = USPS.new(:login => '123YOURNAME456', :test => true)
|
10
|
-
# usps.valid_credentials?
|
11
|
-
#
|
12
|
-
# This will send a test request to the USPS test servers, which they ask you
|
13
|
-
# to do before they put your API key in production mode.
|
14
|
-
class USPS < Carrier
|
15
|
-
EventDetails = Struct.new(:description, :time, :zoneless_time, :location)
|
16
|
-
EVENT_MESSAGE_PATTERNS = [
|
17
|
-
/^(.*), (\w+ \d{1,2}, \d{4}, \d{1,2}:\d\d [ap]m), (.*), (\w\w) (\d{5})$/i,
|
18
|
-
/^Your item \w{2,3} (out for delivery|delivered) at (\d{1,2}:\d\d [ap]m on \w+ \d{1,2}, \d{4}) in (.*), (\w\w) (\d{5})\.$/i
|
19
|
-
]
|
20
|
-
self.retry_safe = true
|
21
|
-
|
22
|
-
cattr_reader :name
|
23
|
-
@@name = "USPS"
|
24
|
-
|
25
|
-
LIVE_DOMAIN = 'production.shippingapis.com'
|
26
|
-
LIVE_RESOURCE = 'ShippingAPI.dll'
|
27
|
-
|
28
|
-
TEST_DOMAINS = { # indexed by security; e.g. TEST_DOMAINS[USE_SSL[:rates]]
|
29
|
-
true => 'secure.shippingapis.com',
|
30
|
-
false => 'testing.shippingapis.com'
|
31
|
-
}
|
32
|
-
|
33
|
-
TEST_RESOURCE = 'ShippingAPITest.dll'
|
34
|
-
|
35
|
-
API_CODES = {
|
36
|
-
:us_rates => 'RateV4',
|
37
|
-
:world_rates => 'IntlRateV2',
|
38
|
-
:test => 'CarrierPickupAvailability',
|
39
|
-
:track => 'TrackV2'
|
40
|
-
}
|
41
|
-
USE_SSL = {
|
42
|
-
:us_rates => false,
|
43
|
-
:world_rates => false,
|
44
|
-
:test => true,
|
45
|
-
:track => false
|
46
|
-
}
|
47
|
-
|
48
|
-
CONTAINERS = {
|
49
|
-
rectangular: 'RECTANGULAR',
|
50
|
-
variable: 'VARIABLE',
|
51
|
-
box: 'FLAT RATE BOX',
|
52
|
-
box_large: 'LG FLAT RATE BOX',
|
53
|
-
box_medium: 'MD FLAT RATE BOX',
|
54
|
-
box_small: 'SM FLAT RATE BOX',
|
55
|
-
envelope: 'FLAT RATE ENVELOPE',
|
56
|
-
envelope_legal: 'LEGAL FLAT RATE ENVELOPE',
|
57
|
-
envelope_padded: 'PADDED FLAT RATE ENVELOPE',
|
58
|
-
envelope_gift_card: 'GIFT CARD FLAT RATE ENVELOPE',
|
59
|
-
envelope_window: 'WINDOW FLAT RATE ENVELOPE',
|
60
|
-
envelope_small: 'SM FLAT RATE ENVELOPE',
|
61
|
-
package_service: 'PACKAGE SERVICE'
|
62
|
-
}
|
63
|
-
|
64
|
-
MAIL_TYPES = {
|
65
|
-
:package => 'Package',
|
66
|
-
:postcard => 'Postcards or aerogrammes',
|
67
|
-
:matter_for_the_blind => 'Matter for the blind',
|
68
|
-
:envelope => 'Envelope'
|
69
|
-
}
|
70
|
-
|
71
|
-
PACKAGE_PROPERTIES = {
|
72
|
-
'ZipOrigination' => :origin_zip,
|
73
|
-
'ZipDestination' => :destination_zip,
|
74
|
-
'Pounds' => :pounds,
|
75
|
-
'Ounces' => :ounces,
|
76
|
-
'Container' => :container,
|
77
|
-
'Size' => :size,
|
78
|
-
'Machinable' => :machinable,
|
79
|
-
'Zone' => :zone,
|
80
|
-
'Postage' => :postage,
|
81
|
-
'Restrictions' => :restrictions
|
82
|
-
}
|
83
|
-
POSTAGE_PROPERTIES = {
|
84
|
-
'MailService' => :service,
|
85
|
-
'Rate' => :rate
|
86
|
-
}
|
87
|
-
US_SERVICES = {
|
88
|
-
:first_class => 'FIRST CLASS',
|
89
|
-
:priority => 'PRIORITY',
|
90
|
-
:express => 'EXPRESS',
|
91
|
-
:bpm => 'BPM',
|
92
|
-
:parcel => 'PARCEL',
|
93
|
-
:media => 'MEDIA',
|
94
|
-
:library => 'LIBRARY',
|
95
|
-
:online => 'ONLINE',
|
96
|
-
:plus => 'PLUS',
|
97
|
-
:all => 'ALL'
|
98
|
-
}
|
99
|
-
DEFAULT_SERVICE = Hash.new(:all).update(
|
100
|
-
:base => :online,
|
101
|
-
:plus => :plus
|
102
|
-
)
|
103
|
-
DOMESTIC_RATE_FIELD = Hash.new('Rate').update(
|
104
|
-
:base => 'CommercialRate',
|
105
|
-
:plus => 'CommercialPlusRate'
|
106
|
-
)
|
107
|
-
INTERNATIONAL_RATE_FIELD = Hash.new('Postage').update(
|
108
|
-
:base => 'CommercialPostage',
|
109
|
-
:plus => 'CommercialPlusPostage'
|
110
|
-
)
|
111
|
-
COMMERCIAL_FLAG_NAME = {
|
112
|
-
:base => 'CommercialFlag',
|
113
|
-
:plus => 'CommercialPlusFlag'
|
114
|
-
}
|
115
|
-
FIRST_CLASS_MAIL_TYPES = {
|
116
|
-
:letter => 'LETTER',
|
117
|
-
:flat => 'FLAT',
|
118
|
-
:parcel => 'PARCEL',
|
119
|
-
:post_card => 'POSTCARD',
|
120
|
-
:package_service => 'PACKAGESERVICE'
|
121
|
-
}
|
122
|
-
|
123
|
-
# Array of U.S. possessions according to USPS: https://www.usps.com/ship/official-abbreviations.htm
|
124
|
-
US_POSSESSIONS = %w(AS FM GU MH MP PW PR VI)
|
125
|
-
|
126
|
-
# TODO: figure out how USPS likes to say "Ivory Coast"
|
127
|
-
#
|
128
|
-
# Country names:
|
129
|
-
# http://pe.usps.gov/text/Imm/immctry.htm
|
130
|
-
COUNTRY_NAME_CONVERSIONS = {
|
131
|
-
"BA" => "Bosnia-Herzegovina",
|
132
|
-
"CD" => "Congo, Democratic Republic of the",
|
133
|
-
"CG" => "Congo (Brazzaville),Republic of the",
|
134
|
-
"CI" => "Côte d'Ivoire (Ivory Coast)",
|
135
|
-
"CK" => "Cook Islands (New Zealand)",
|
136
|
-
"FK" => "Falkland Islands",
|
137
|
-
"GB" => "Great Britain and Northern Ireland",
|
138
|
-
"GE" => "Georgia, Republic of",
|
139
|
-
"IR" => "Iran",
|
140
|
-
"KN" => "Saint Kitts (St. Christopher and Nevis)",
|
141
|
-
"KP" => "North Korea (Korea, Democratic People's Republic of)",
|
142
|
-
"KR" => "South Korea (Korea, Republic of)",
|
143
|
-
"LA" => "Laos",
|
144
|
-
"LY" => "Libya",
|
145
|
-
"MC" => "Monaco (France)",
|
146
|
-
"MD" => "Moldova",
|
147
|
-
"MK" => "Macedonia, Republic of",
|
148
|
-
"MM" => "Burma",
|
149
|
-
"PN" => "Pitcairn Island",
|
150
|
-
"RU" => "Russia",
|
151
|
-
"SK" => "Slovak Republic",
|
152
|
-
"TK" => "Tokelau (Union) Group (Western Samoa)",
|
153
|
-
"TW" => "Taiwan",
|
154
|
-
"TZ" => "Tanzania",
|
155
|
-
"VA" => "Vatican City",
|
156
|
-
"VG" => "British Virgin Islands",
|
157
|
-
"VN" => "Vietnam",
|
158
|
-
"WF" => "Wallis and Futuna Islands",
|
159
|
-
"WS" => "Western Samoa"
|
160
|
-
}
|
161
|
-
|
162
|
-
STATUS_NODE_PATTERNS = %w(
|
163
|
-
Error/Description
|
164
|
-
*/TrackInfo/Error/Description
|
165
|
-
)
|
166
|
-
|
167
|
-
RESPONSE_ERROR_MESSAGES = [
|
168
|
-
/There is no record of that mail item/,
|
169
|
-
/This Information has not been included in this Test Server\./,
|
170
|
-
/Delivery status information is not available/
|
171
|
-
]
|
172
|
-
|
173
|
-
ESCAPING_AND_SYMBOLS = /&lt;\S*&gt;/
|
174
|
-
LEADING_USPS = /^USPS/
|
175
|
-
TRAILING_ASTERISKS = /\*+$/
|
176
|
-
SERVICE_NAME_SUBSTITUTIONS = /#{ESCAPING_AND_SYMBOLS}|#{LEADING_USPS}|#{TRAILING_ASTERISKS}/
|
177
|
-
|
178
|
-
def find_tracking_info(tracking_number, options = {})
|
179
|
-
options = @options.update(options)
|
180
|
-
tracking_request = build_tracking_request(tracking_number, options)
|
181
|
-
response = commit(:track, tracking_request, (options[:test] || false))
|
182
|
-
parse_tracking_response(response, options)
|
183
|
-
end
|
184
|
-
|
185
|
-
def self.size_code_for(package)
|
186
|
-
if package.inches(:max) <= 12
|
187
|
-
'REGULAR'
|
188
|
-
else
|
189
|
-
'LARGE'
|
190
|
-
end
|
191
|
-
end
|
192
|
-
|
193
|
-
# from info at http://www.usps.com/businessmail101/mailcharacteristics/parcels.htm
|
194
|
-
#
|
195
|
-
# package.options[:books] -- 25 lb. limit instead of 35 for books or other printed matter.
|
196
|
-
# Defaults to false.
|
197
|
-
def self.package_machinable?(package, options = {})
|
198
|
-
at_least_minimum = package.inches(:length) >= 6.0 &&
|
199
|
-
package.inches(:width) >= 3.0 &&
|
200
|
-
package.inches(:height) >= 0.25 &&
|
201
|
-
package.ounces >= 6.0
|
202
|
-
at_most_maximum = package.inches(:length) <= 34.0 &&
|
203
|
-
package.inches(:width) <= 17.0 &&
|
204
|
-
package.inches(:height) <= 17.0 &&
|
205
|
-
package.pounds <= (package.options[:books] ? 25.0 : 35.0)
|
206
|
-
at_least_minimum && at_most_maximum
|
207
|
-
end
|
208
|
-
|
209
|
-
def requirements
|
210
|
-
[:login]
|
211
|
-
end
|
212
|
-
|
213
|
-
def find_rates(origin, destination, packages, options = {})
|
214
|
-
options = @options.merge(options)
|
215
|
-
|
216
|
-
origin = Location.from(origin)
|
217
|
-
destination = Location.from(destination)
|
218
|
-
packages = Array(packages)
|
219
|
-
|
220
|
-
domestic_codes = US_POSSESSIONS + ['US', nil]
|
221
|
-
if domestic_codes.include?(destination.country_code(:alpha2))
|
222
|
-
us_rates(origin, destination, packages, options)
|
223
|
-
else
|
224
|
-
world_rates(origin, destination, packages, options)
|
225
|
-
end
|
226
|
-
end
|
227
|
-
|
228
|
-
def valid_credentials?
|
229
|
-
# Cannot test with find_rates because USPS doesn't allow that in test mode
|
230
|
-
test_mode? ? canned_address_verification_works? : super
|
231
|
-
end
|
232
|
-
|
233
|
-
def maximum_weight
|
234
|
-
Mass.new(70, :pounds)
|
235
|
-
end
|
236
|
-
|
237
|
-
def extract_event_details(message)
|
238
|
-
return EventDetails.new unless EVENT_MESSAGE_PATTERNS.any? { |pattern| message =~ pattern }
|
239
|
-
description = $1.upcase
|
240
|
-
timestamp = $2
|
241
|
-
city = $3
|
242
|
-
state = $4
|
243
|
-
zip_code = $5
|
244
|
-
|
245
|
-
time = Time.parse(timestamp)
|
246
|
-
zoneless_time = Time.utc(time.year, time.month, time.mday, time.hour, time.min, time.sec)
|
247
|
-
location = Location.new(city: city, state: state, postal_code: zip_code, country: 'USA')
|
248
|
-
EventDetails.new(description, time, zoneless_time, location)
|
249
|
-
end
|
250
|
-
|
251
|
-
protected
|
252
|
-
|
253
|
-
def build_tracking_request(tracking_number, options = {})
|
254
|
-
xml_request = XmlNode.new('TrackRequest', 'USERID' => @options[:login]) do |root_node|
|
255
|
-
root_node << XmlNode.new('TrackID', :ID => tracking_number)
|
256
|
-
end
|
257
|
-
URI.encode(xml_request.to_s)
|
258
|
-
end
|
259
|
-
|
260
|
-
def us_rates(origin, destination, packages, options = {})
|
261
|
-
request = build_us_rate_request(packages, origin.zip, destination.zip, options)
|
262
|
-
# never use test mode; rate requests just won't work on test servers
|
263
|
-
parse_rate_response origin, destination, packages, commit(:us_rates, request, false), options
|
264
|
-
end
|
265
|
-
|
266
|
-
def world_rates(origin, destination, packages, options = {})
|
267
|
-
request = build_world_rate_request(origin, packages, destination, options)
|
268
|
-
# never use test mode; rate requests just won't work on test servers
|
269
|
-
parse_rate_response origin, destination, packages, commit(:world_rates, request, false), options
|
270
|
-
end
|
271
|
-
|
272
|
-
# Once the address verification API is implemented, remove this and have valid_credentials? build the request using that instead.
|
273
|
-
def canned_address_verification_works?
|
274
|
-
return false unless @options[:login]
|
275
|
-
request = <<-EOF
|
276
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
277
|
-
<CarrierPickupAvailabilityRequest USERID="#{URI.encode(@options[:login])}">
|
278
|
-
<FirmName>Shopifolk</FirmName>
|
279
|
-
<SuiteOrApt>Suite 0</SuiteOrApt>
|
280
|
-
<Address2>18 Fair Ave</Address2>
|
281
|
-
<Urbanization />
|
282
|
-
<City>San Francisco</City>
|
283
|
-
<State>CA</State>
|
284
|
-
<ZIP5>94110</ZIP5>
|
285
|
-
<ZIP4>9411</ZIP4>
|
286
|
-
</CarrierPickupAvailabilityRequest>
|
287
|
-
EOF
|
288
|
-
xml = REXML::Document.new(commit(:test, URI.encode(request), true))
|
289
|
-
xml.get_text('/CarrierPickupAvailabilityResponse/City').to_s == 'SAN FRANCISCO' && xml.get_text('/CarrierPickupAvailabilityResponse/Address2').to_s == '18 FAIR AVE'
|
290
|
-
end
|
291
|
-
|
292
|
-
# options[:service] -- One of [:first_class, :priority, :express, :bpm, :parcel,
|
293
|
-
# :media, :library, :online, :plus, :all]. defaults to :all.
|
294
|
-
# options[:books] -- Either true or false. Packages of books or other printed matter
|
295
|
-
# have a lower weight limit to be considered machinable.
|
296
|
-
# package.options[:container] -- Can be :rectangular, :variable, or a flat rate container
|
297
|
-
# defined in CONTAINERS.
|
298
|
-
# package.options[:machinable] -- Either true or false. Overrides the detection of
|
299
|
-
# "machinability" entirely.
|
300
|
-
def build_us_rate_request(packages, origin_zip, destination_zip, options = {})
|
301
|
-
packages = Array(packages)
|
302
|
-
request = XmlNode.new('RateV4Request', :USERID => @options[:login]) do |rate_request|
|
303
|
-
packages.each_with_index do |p, id|
|
304
|
-
rate_request << XmlNode.new('Package', :ID => id.to_s) do |package|
|
305
|
-
commercial_type = commercial_type(options)
|
306
|
-
default_service = DEFAULT_SERVICE[commercial_type]
|
307
|
-
service = options.fetch(:service, default_service).to_sym
|
308
|
-
|
309
|
-
if commercial_type && service != default_service
|
310
|
-
raise ArgumentError, "Commercial #{commercial_type} rates are only provided with the #{default_service.inspect} service."
|
311
|
-
end
|
312
|
-
|
313
|
-
package << XmlNode.new('Service', US_SERVICES[service])
|
314
|
-
package << XmlNode.new('FirstClassMailType', FIRST_CLASS_MAIL_TYPES[options[:first_class_mail_type].try(:to_sym)])
|
315
|
-
package << XmlNode.new('ZipOrigination', strip_zip(origin_zip))
|
316
|
-
package << XmlNode.new('ZipDestination', strip_zip(destination_zip))
|
317
|
-
package << XmlNode.new('Pounds', 0)
|
318
|
-
package << XmlNode.new('Ounces', "%0.1f" % [p.ounces, 1].max)
|
319
|
-
package << XmlNode.new('Container', CONTAINERS[p.options[:container]])
|
320
|
-
package << XmlNode.new('Size', USPS.size_code_for(p))
|
321
|
-
package << XmlNode.new('Width', "%0.2f" % p.inches(:width))
|
322
|
-
package << XmlNode.new('Length', "%0.2f" % p.inches(:length))
|
323
|
-
package << XmlNode.new('Height', "%0.2f" % p.inches(:height))
|
324
|
-
package << XmlNode.new('Girth', "%0.2f" % p.inches(:girth))
|
325
|
-
is_machinable = if p.options.has_key?(:machinable)
|
326
|
-
p.options[:machinable] ? true : false
|
327
|
-
else
|
328
|
-
USPS.package_machinable?(p)
|
329
|
-
end
|
330
|
-
package << XmlNode.new('Machinable', is_machinable.to_s.upcase)
|
331
|
-
end
|
332
|
-
end
|
333
|
-
end
|
334
|
-
URI.encode(save_request(request.to_s))
|
335
|
-
end
|
336
|
-
|
337
|
-
# important difference with international rate requests:
|
338
|
-
# * services are not given in the request
|
339
|
-
# * package sizes are not given in the request
|
340
|
-
# * services are returned in the response along with restrictions of size
|
341
|
-
# * the size restrictions are returned AS AN ENGLISH SENTENCE (!?)
|
342
|
-
#
|
343
|
-
#
|
344
|
-
# package.options[:mail_type] -- one of [:package, :postcard, :matter_for_the_blind, :envelope].
|
345
|
-
# Defaults to :package.
|
346
|
-
def build_world_rate_request(origin, packages, destination, options)
|
347
|
-
country = COUNTRY_NAME_CONVERSIONS[destination.country.code(:alpha2).value] || destination.country.name
|
348
|
-
request = XmlNode.new('IntlRateV2Request', :USERID => @options[:login]) do |rate_request|
|
349
|
-
rate_request << XmlNode.new('Revision', 2)
|
350
|
-
packages.each_index do |id|
|
351
|
-
p = packages[id]
|
352
|
-
rate_request << XmlNode.new('Package', :ID => id.to_s) do |package|
|
353
|
-
package << XmlNode.new('Pounds', 0)
|
354
|
-
package << XmlNode.new('Ounces', [p.ounces, 1].max.ceil) # takes an integer for some reason, must be rounded UP
|
355
|
-
package << XmlNode.new('MailType', MAIL_TYPES[p.options[:mail_type]] || 'Package')
|
356
|
-
package << XmlNode.new('GXG') do |gxg|
|
357
|
-
gxg << XmlNode.new('POBoxFlag', destination.po_box? ? 'Y' : 'N')
|
358
|
-
gxg << XmlNode.new('GiftFlag', p.gift? ? 'Y' : 'N')
|
359
|
-
end
|
360
|
-
value = if p.value && p.value > 0 && p.currency && p.currency != 'USD'
|
361
|
-
0.0
|
362
|
-
else
|
363
|
-
(p.value || 0) / 100.0
|
364
|
-
end
|
365
|
-
package << XmlNode.new('ValueOfContents', value)
|
366
|
-
package << XmlNode.new('Country') do |node|
|
367
|
-
node.cdata = country
|
368
|
-
end
|
369
|
-
package << XmlNode.new('Container', p.cylinder? ? 'NONRECTANGULAR' : 'RECTANGULAR')
|
370
|
-
package << XmlNode.new('Size', USPS.size_code_for(p))
|
371
|
-
package << XmlNode.new('Width', "%0.2f" % [p.inches(:width), 0.01].max)
|
372
|
-
package << XmlNode.new('Length', "%0.2f" % [p.inches(:length), 0.01].max)
|
373
|
-
package << XmlNode.new('Height', "%0.2f" % [p.inches(:height), 0.01].max)
|
374
|
-
package << XmlNode.new('Girth', "%0.2f" % [p.inches(:girth), 0.01].max)
|
375
|
-
if commercial_type = commercial_type(options)
|
376
|
-
package << XmlNode.new(COMMERCIAL_FLAG_NAME.fetch(commercial_type), 'Y')
|
377
|
-
end
|
378
|
-
package << XmlNode.new('OriginZip', origin.zip)
|
379
|
-
package << XmlNode.new('AcceptanceDateTime', (options[:acceptance_time] || Time.now.utc).iso8601)
|
380
|
-
package << XmlNode.new('DestinationPostalCode', destination.zip)
|
381
|
-
end
|
382
|
-
end
|
383
|
-
end
|
384
|
-
URI.encode(save_request(request.to_s))
|
385
|
-
end
|
386
|
-
|
387
|
-
def parse_rate_response(origin, destination, packages, response, options = {})
|
388
|
-
success = true
|
389
|
-
message = ''
|
390
|
-
rate_hash = {}
|
391
|
-
|
392
|
-
xml = REXML::Document.new(response)
|
393
|
-
|
394
|
-
if error = xml.elements['/Error']
|
395
|
-
success = false
|
396
|
-
message = error.elements['Description'].text
|
397
|
-
else
|
398
|
-
xml.elements.each('/*/Package') do |package|
|
399
|
-
if package.elements['Error']
|
400
|
-
success = false
|
401
|
-
message = package.get_text('Error/Description').to_s
|
402
|
-
break
|
403
|
-
end
|
404
|
-
end
|
405
|
-
|
406
|
-
if success
|
407
|
-
rate_hash = rates_from_response_node(xml, packages, options)
|
408
|
-
unless rate_hash
|
409
|
-
success = false
|
410
|
-
message = "Unknown root node in XML response: '#{xml.root.name}'"
|
411
|
-
end
|
412
|
-
end
|
413
|
-
|
414
|
-
end
|
415
|
-
|
416
|
-
if success
|
417
|
-
rate_estimates = rate_hash.keys.map do |service_name|
|
418
|
-
RateEstimate.new(origin, destination, @@name, "USPS #{service_name}",
|
419
|
-
:package_rates => rate_hash[service_name][:package_rates],
|
420
|
-
:service_code => rate_hash[service_name][:service_code],
|
421
|
-
:currency => 'USD')
|
422
|
-
end
|
423
|
-
rate_estimates.reject! { |e| e.package_count != packages.length }
|
424
|
-
rate_estimates = rate_estimates.sort_by(&:total_price)
|
425
|
-
end
|
426
|
-
|
427
|
-
RateResponse.new(success, message, Hash.from_xml(response), :rates => rate_estimates, :xml => response, :request => last_request)
|
428
|
-
end
|
429
|
-
|
430
|
-
def rates_from_response_node(response_node, packages, options = {})
|
431
|
-
rate_hash = {}
|
432
|
-
return false unless (root_node = response_node.elements['/IntlRateV2Response | /RateV4Response'])
|
433
|
-
|
434
|
-
commercial_type = commercial_type(options)
|
435
|
-
service_node, service_code_node, service_name_node, rate_node = if root_node.name == 'RateV4Response'
|
436
|
-
%w(Postage CLASSID MailService) << DOMESTIC_RATE_FIELD[commercial_type]
|
437
|
-
else
|
438
|
-
%w(Service ID SvcDescription) << INTERNATIONAL_RATE_FIELD[commercial_type]
|
439
|
-
end
|
440
|
-
|
441
|
-
root_node.each_element('Package') do |package_node|
|
442
|
-
this_package = packages[package_node.attributes['ID'].to_i]
|
443
|
-
|
444
|
-
package_node.each_element(service_node) do |service_response_node|
|
445
|
-
service_name = service_response_node.get_text(service_name_node).to_s
|
446
|
-
|
447
|
-
service_name.gsub!(SERVICE_NAME_SUBSTITUTIONS, '')
|
448
|
-
service_name.strip!
|
449
|
-
|
450
|
-
# aggregate specific package rates into a service-centric RateEstimate
|
451
|
-
# first package with a given service name will initialize these;
|
452
|
-
# later packages with same service will add to them
|
453
|
-
this_service = rate_hash[service_name] ||= {}
|
454
|
-
this_service[:service_code] ||= service_response_node.attributes[service_code_node]
|
455
|
-
package_rates = this_service[:package_rates] ||= []
|
456
|
-
this_package_rate = {:package => this_package,
|
457
|
-
:rate => Package.cents_from(rate_value(rate_node, service_response_node, commercial_type))}
|
458
|
-
|
459
|
-
package_rates << this_package_rate if package_valid_for_service(this_package, service_response_node)
|
460
|
-
end
|
461
|
-
end
|
462
|
-
rate_hash
|
463
|
-
end
|
464
|
-
|
465
|
-
def package_valid_for_service(package, service_node)
|
466
|
-
return true if service_node.elements['MaxWeight'].nil?
|
467
|
-
max_weight = service_node.get_text('MaxWeight').to_s.to_f
|
468
|
-
name = service_node.get_text('SvcDescription | MailService').to_s.downcase
|
469
|
-
|
470
|
-
if name =~ /flat.rate.box/ # domestic or international flat rate box
|
471
|
-
# flat rate dimensions from http://www.usps.com/shipping/flatrate.htm
|
472
|
-
return (package_valid_for_max_dimensions(package,
|
473
|
-
:weight => max_weight, # domestic apparently has no weight restriction
|
474
|
-
:length => 11.0,
|
475
|
-
:width => 8.5,
|
476
|
-
:height => 5.5) or
|
477
|
-
package_valid_for_max_dimensions(package,
|
478
|
-
:weight => max_weight,
|
479
|
-
:length => 13.625,
|
480
|
-
:width => 11.875,
|
481
|
-
:height => 3.375))
|
482
|
-
elsif name =~ /flat.rate.envelope/
|
483
|
-
return package_valid_for_max_dimensions(package,
|
484
|
-
:weight => max_weight,
|
485
|
-
:length => 12.5,
|
486
|
-
:width => 9.5,
|
487
|
-
:height => 0.75)
|
488
|
-
elsif service_node.elements['MailService'] # domestic non-flat rates
|
489
|
-
return true
|
490
|
-
else # international non-flat rates
|
491
|
-
# Some sample english that this is required to parse:
|
492
|
-
#
|
493
|
-
# 'Max. length 46", width 35", height 46" and max. length plus girth 108"'
|
494
|
-
# 'Max. length 24", Max. length, height, depth combined 36"'
|
495
|
-
#
|
496
|
-
sentence = CGI.unescapeHTML(service_node.get_text('MaxDimensions').to_s)
|
497
|
-
tokens = sentence.downcase.split(/[^\d]*"/).reject(&:empty?)
|
498
|
-
max_dimensions = {:weight => max_weight}
|
499
|
-
single_axis_values = []
|
500
|
-
tokens.each do |token|
|
501
|
-
axis_sum = [/length/, /width/, /height/, /depth/].sum { |regex| (token =~ regex) ? 1 : 0 }
|
502
|
-
unless axis_sum == 0
|
503
|
-
value = token[/\d+$/].to_f
|
504
|
-
if axis_sum == 3
|
505
|
-
max_dimensions[:length_plus_width_plus_height] = value
|
506
|
-
elsif token =~ /girth/ and axis_sum == 1
|
507
|
-
max_dimensions[:length_plus_girth] = value
|
508
|
-
else
|
509
|
-
single_axis_values << value
|
510
|
-
end
|
511
|
-
end
|
512
|
-
end
|
513
|
-
single_axis_values.sort!.reverse!
|
514
|
-
[:length, :width, :height].each_with_index do |axis, i|
|
515
|
-
max_dimensions[axis] = single_axis_values[i] if single_axis_values[i]
|
516
|
-
end
|
517
|
-
package_valid_for_max_dimensions(package, max_dimensions)
|
518
|
-
end
|
519
|
-
end
|
520
|
-
|
521
|
-
def package_valid_for_max_dimensions(package, dimensions)
|
522
|
-
((not ([:length, :width, :height].map { |dim| dimensions[dim].nil? || dimensions[dim].to_f >= package.inches(dim).to_f }.include?(false))) and
|
523
|
-
(dimensions[:weight].nil? || dimensions[:weight] >= package.pounds) and
|
524
|
-
(dimensions[:length_plus_girth].nil? or
|
525
|
-
dimensions[:length_plus_girth].to_f >=
|
526
|
-
package.inches(:length) + package.inches(:girth)) and
|
527
|
-
(dimensions[:length_plus_width_plus_height].nil? or
|
528
|
-
dimensions[:length_plus_width_plus_height].to_f >=
|
529
|
-
package.inches(:length) + package.inches(:width) + package.inches(:height)))
|
530
|
-
end
|
531
|
-
|
532
|
-
def parse_tracking_response(response, options)
|
533
|
-
actual_delivery_date, status = nil
|
534
|
-
xml = REXML::Document.new(response)
|
535
|
-
root_node = xml.elements['TrackResponse']
|
536
|
-
|
537
|
-
success = response_success?(xml)
|
538
|
-
message = response_message(xml)
|
539
|
-
|
540
|
-
if success
|
541
|
-
destination = nil
|
542
|
-
shipment_events = []
|
543
|
-
tracking_details = xml.elements.collect('*/*/TrackDetail') { |e| e }
|
544
|
-
|
545
|
-
tracking_summary = xml.elements.collect('*/*/TrackSummary') { |e| e }.first
|
546
|
-
tracking_details << tracking_summary
|
547
|
-
|
548
|
-
tracking_number = root_node.elements['TrackInfo'].attributes['ID'].to_s
|
549
|
-
|
550
|
-
tracking_details.each do |event|
|
551
|
-
details = extract_event_details(event.get_text.to_s)
|
552
|
-
shipment_events << ShipmentEvent.new(details.description, details.zoneless_time, details.location) if details.location
|
553
|
-
end
|
554
|
-
|
555
|
-
shipment_events = shipment_events.sort_by(&:time)
|
556
|
-
|
557
|
-
if last_shipment = shipment_events.last
|
558
|
-
status = last_shipment.status
|
559
|
-
actual_delivery_date = last_shipment.time if last_shipment.delivered?
|
560
|
-
end
|
561
|
-
end
|
562
|
-
|
563
|
-
TrackingResponse.new(success, message, Hash.from_xml(response),
|
564
|
-
:carrier => @@name,
|
565
|
-
:xml => response,
|
566
|
-
:request => last_request,
|
567
|
-
:shipment_events => shipment_events,
|
568
|
-
:destination => destination,
|
569
|
-
:tracking_number => tracking_number,
|
570
|
-
:status => status,
|
571
|
-
:actual_delivery_date => actual_delivery_date
|
572
|
-
)
|
573
|
-
end
|
574
|
-
|
575
|
-
def track_summary_node(document)
|
576
|
-
document.elements['*/*/TrackSummary']
|
577
|
-
end
|
578
|
-
|
579
|
-
def error_description_node(document)
|
580
|
-
STATUS_NODE_PATTERNS.each do |pattern|
|
581
|
-
if node = document.elements[pattern]
|
582
|
-
return node
|
583
|
-
end
|
584
|
-
end
|
585
|
-
end
|
586
|
-
|
587
|
-
def response_status_node(document)
|
588
|
-
track_summary_node(document) || error_description_node(document)
|
589
|
-
end
|
590
|
-
|
591
|
-
def has_error?(document)
|
592
|
-
!!document.elements['Error']
|
593
|
-
end
|
594
|
-
|
595
|
-
def no_record?(document)
|
596
|
-
summary_node = track_summary_node(document)
|
597
|
-
if summary_node
|
598
|
-
summary = summary_node.get_text.to_s
|
599
|
-
RESPONSE_ERROR_MESSAGES.detect { |re| summary =~ re }
|
600
|
-
summary =~ /There is no record of that mail item/ || summary =~ /This Information has not been included in this Test Server\./
|
601
|
-
else
|
602
|
-
false
|
603
|
-
end
|
604
|
-
end
|
605
|
-
|
606
|
-
def tracking_info_error?(document)
|
607
|
-
document.elements['*/TrackInfo/Error']
|
608
|
-
end
|
609
|
-
|
610
|
-
def response_success?(document)
|
611
|
-
!(has_error?(document) || no_record?(document) || tracking_info_error?(document))
|
612
|
-
end
|
613
|
-
|
614
|
-
def response_message(document)
|
615
|
-
response_node = response_status_node(document)
|
616
|
-
response_node.get_text.to_s
|
617
|
-
end
|
618
|
-
|
619
|
-
def commit(action, request, test = false)
|
620
|
-
ssl_get(request_url(action, request, test))
|
621
|
-
end
|
622
|
-
|
623
|
-
def request_url(action, request, test)
|
624
|
-
scheme = USE_SSL[action] ? 'https://' : 'http://'
|
625
|
-
host = test ? TEST_DOMAINS[USE_SSL[action]] : LIVE_DOMAIN
|
626
|
-
resource = test ? TEST_RESOURCE : LIVE_RESOURCE
|
627
|
-
"#{scheme}#{host}/#{resource}?API=#{API_CODES[action]}&XML=#{request}"
|
628
|
-
end
|
629
|
-
|
630
|
-
def strip_zip(zip)
|
631
|
-
zip.to_s.scan(/\d{5}/).first || zip
|
632
|
-
end
|
633
|
-
|
634
|
-
private
|
635
|
-
|
636
|
-
def rate_value(rate_node, service_response_node, commercial_type)
|
637
|
-
service_response_node.get_text(rate_node).to_s.to_f
|
638
|
-
end
|
639
|
-
|
640
|
-
def commercial_type(options)
|
641
|
-
if options[:commercial_plus] == true
|
642
|
-
:plus
|
643
|
-
elsif options[:commercial_base] == true
|
644
|
-
:base
|
645
|
-
end
|
646
|
-
end
|
647
|
-
end
|
648
|
-
end
|
649
|
-
end
|