ShippingInfo 2.0

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.
@@ -0,0 +1,357 @@
1
+ require 'date'
2
+ require 'time'
3
+ require 'rexml/document'
4
+ require 'net/http'
5
+ require 'net/https'
6
+ require 'rubygems'
7
+ require 'active_support'
8
+
9
+ module Fedex
10
+ # Provides a simple api to to ups's time in transit service.
11
+ class UpsInfo
12
+
13
+ TEST_URL = "https://wwwcie.ups.com/ups.app/xml"
14
+
15
+ # UPS Production URL
16
+ PRODUCTION_URL = "https://onlinetools.ups.com/ups.app/xml"
17
+
18
+ XPCI_VERSION = '1.0002'
19
+ DEFAULT_CUTOFF_TIME = 14
20
+ DEFAULT_TIMEOUT = 30
21
+ DEFAULT_RETRY_COUNT = 3
22
+ DEFAULT_COUNTRY_CODE = 'US'
23
+ DEFAULT_UNIT_OF_MEASUREMENT = 'LBS'
24
+
25
+ def initialize(access_options)
26
+ @order_cutoff_time = access_options[:order_cutoff_time] || DEFAULT_CUTOFF_TIME
27
+ @timeout = access_options[:timeout] || DEFAULT_TIMEOUT
28
+ @retry_count = access_options[:retry_count] || DEFAULT_CUTOFF_TIME
29
+
30
+ @access_xml = generate_xml({
31
+ :AccessRequest => {
32
+ :AccessLicenseNumber => access_options[:access_license_number],
33
+ :UserId => access_options[:user_id],
34
+ :Password => access_options[:password]
35
+ }
36
+ })
37
+
38
+ @transit_from_attributes = {
39
+ :AddressArtifactFormat => {
40
+ :PoliticalDivision2 => access_options[:sender_city],
41
+ :PoliticalDivision1 => access_options[:sender_state],
42
+ :CountryCode => access_options[:sender_country_code] || DEFAULT_COUNTRY_CODE,
43
+ :PostcodePrimaryLow => access_options[:sender_zip]
44
+ }
45
+ }
46
+
47
+ @rate_from_attributes = {
48
+ :Address => {
49
+ :City => access_options[:sender_city],
50
+ :StateProvinceCode => access_options[:sender_state],
51
+ :CountryCode => access_options[:sender_country_code] || DEFAULT_COUNTRY_CODE,
52
+ :PostalCode => access_options[:sender_zip]
53
+ }
54
+ }
55
+
56
+ end
57
+
58
+ def getPrice (options)
59
+
60
+ @url = options[:mode] == "production" ? PRODUCTION_URL : TEST_URL + '/Rate'
61
+
62
+ #@url = options[:url] + '/Rate'
63
+ # build our request xml
64
+
65
+ xml = @access_xml + generate_xml(build_price_attributes(options))
66
+ #puts xml
67
+ # attempt the request in a timeout
68
+ delivery_price = 0
69
+
70
+ begin
71
+ Timeout.timeout(@timeout) do
72
+ response = send_request(@url, xml)
73
+ delivery_price = response_to_price(response)
74
+ end
75
+ delivery_price
76
+ end
77
+ end
78
+
79
+ def getTransitTime(options)
80
+
81
+ @url = options[:mode] == "production" ? PRODUCTION_URL : TEST_URL + '/TimeInTransit'
82
+ #@url = options[:url] + '/TimeInTransit'
83
+ # build our request xml
84
+ pickup_date = calculate_pickup_date
85
+ options[:pickup_date] = pickup_date.strftime('%Y%m%d')
86
+ xml = @access_xml + generate_xml(build_transit_attributes(options))
87
+
88
+ # attempt the request in a timeout
89
+ delivery_dates = {}
90
+ attempts = 0
91
+ begin
92
+ Timeout.timeout(@timeout) do
93
+ response = send_request(@url, xml)
94
+ delivery_dates = response_to_map(response)
95
+ end
96
+
97
+ # We can only attempt to recover from Timeout errors, all other errors
98
+ # should be raised back to the user
99
+ rescue Timeout::Error => error
100
+ if(attempts < @retry_count)
101
+ attempts += 1
102
+ retry
103
+
104
+ else
105
+ raise error
106
+ end
107
+ end
108
+
109
+ delivery_dates
110
+ end
111
+
112
+ private
113
+
114
+ # calculates the next available pickup date based on the current time and the
115
+ # configured order cutoff time
116
+ def calculate_pickup_date
117
+ now = Time.now
118
+ day_of_week = now.strftime('%w').to_i
119
+ in_weekend = [6,0].include?(day_of_week)
120
+ in_friday_after_cutoff = day_of_week == 5 and now.hour > @order_cutoff_time
121
+
122
+ # If we're in a weekend (6 is Sat, 0 is Sun,) or we're in Friday after
123
+ # the cutoff time, then our ship date will move
124
+ if(in_weekend or in_friday_after_cutoff)
125
+ pickup_date = now
126
+
127
+ # if we're in another weekday but after the cutoff time, our ship date
128
+ # moves to tomorrow
129
+ elsif(now.hour > @order_cutoff_time)
130
+ pickup_date = now
131
+ else
132
+ pickup_date = now
133
+ end
134
+ end
135
+
136
+ # Builds a hash of transit request attributes based on the given values
137
+ def build_transit_attributes(options)
138
+ # set defaults if none given
139
+ options[:total_packages] = 1 unless options[:total_packages]
140
+
141
+ # convert all options to string values
142
+ options.each_value {|option| option = options.to_s}
143
+
144
+ transit_attributes = {
145
+ :TimeInTransitRequest => {
146
+ :Request => {
147
+ :RequestAction => 'TimeInTransit',
148
+ :TransactionReference => {
149
+ :XpciVersion => XPCI_VERSION
150
+ }
151
+ },
152
+ :TotalPackagesInShipment => options[:total_packages],
153
+ :ShipmentWeight => {
154
+ :UnitOfMeasurement => {
155
+ :Code => options[:unit_of_measurement] || DEFAULT_UNIT_OF_MEASUREMENT
156
+ },
157
+ :Weight => options[:weight],
158
+ },
159
+ :PickupDate => options[:pickup_date],
160
+ :TransitFrom => @transit_from_attributes,
161
+ :TransitTo => {
162
+ :AddressArtifactFormat => {
163
+ :PoliticalDivision2 => options[:city],
164
+ :PoliticalDivision1 => options[:state],
165
+ :CountryCode => options[:country_code] || DEFAULT_COUNTRY_CODE,
166
+ :PostcodePrimaryLow => options[:zip],
167
+ }
168
+ }
169
+ }
170
+ }
171
+ end
172
+
173
+ # Builds a hash of price attributes based on the given values
174
+ def build_price_attributes(options)
175
+ # convert all options to string values
176
+ options.each_value {|option| option = options.to_s}
177
+
178
+ rate_attributes = {
179
+ :RatingServiceSelectionRequest => {
180
+ :Request => {
181
+ :RequestAction => 'Rate',
182
+ :RequestOption => 'Rate',
183
+ :TransactionReference => {
184
+ :XpciVersion => '1.0'}
185
+ },
186
+ :PickupType => {
187
+ :Code => '01'
188
+ },
189
+ :CustomerClassification => {:Code => '01'},
190
+ :Shipment => {
191
+ :Shipper => @rate_from_attributes,
192
+ :ShipTo => {:Address => {
193
+ :City => options[:city],
194
+ :StateProvinceCode => options[:state],
195
+ :PostalCode => options[:zip],
196
+ :CountryCode => options[:country_code]}
197
+ },
198
+ :Service => {:Code => '03'},
199
+ :Package => {:PackagingType => {:Code => '02'},
200
+ :PackageWeight => {:Weight => options[:weight], :UnitOfMeasurement => 'LBS'}
201
+ }
202
+ }
203
+ }
204
+ }
205
+ end
206
+
207
+ # generates an xml document for the given attributes
208
+ def generate_xml(attributes)
209
+ xml = REXML::Document.new
210
+ xml << REXML::XMLDecl.new
211
+ emit(attributes, xml)
212
+ xml.root.add_attribute("xml:lang", "en-US")
213
+ xml.to_s
214
+ end
215
+
216
+ # recursively emits xml nodes under the given node for values in the given hash
217
+ def emit(attributes, node)
218
+ attributes.each do |k,v|
219
+ child_node = REXML::Element.new(k.to_s, node)
220
+ (v.respond_to? 'each_key') ? emit(v, child_node) : child_node.add_text(v.to_s)
221
+ end
222
+ end
223
+
224
+ # Posts the given data to the given url, returning the raw response
225
+ def send_request(url, data)
226
+ uri = URI.parse(url)
227
+ http = Net::HTTP.new(uri.host, uri.port)
228
+ if uri.port == 443
229
+ http.use_ssl = true
230
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
231
+ end
232
+ response = http.post(uri.path, data)
233
+ response.code == '200' ? response.body : response.error!
234
+ end
235
+
236
+ # converts the given raw xml response to a map of local service codes
237
+ # to estimated delivery dates
238
+ def response_to_map(response)
239
+ response_doc = REXML::Document.new(response)
240
+ response_code = response_doc.elements['//ResponseStatusCode'].text.to_i
241
+ raise "Invalid response from ups:\n#{response_doc.to_s}" if(!response_code || response_code != 1)
242
+ delivery_date = 0
243
+ service_codes_to_delivery_dates = {}
244
+ response_code = response_doc.elements.each('//ServiceSummary') do |service_element|
245
+ service_code = service_element.elements['Service/Code'].text
246
+ if(service_code == "GND")
247
+ date_string = service_element.elements['EstimatedArrival/Date'].text
248
+ pickup_date = service_element.elements['EstimatedArrival/PickupDate'].text
249
+ #time_string = service_element.elements['EstimatedArrival/Time'].text
250
+ #delivery_date = Time.parse("#{date_string}")
251
+ delivery_date = (Time.parse(date_string) - Time.parse(pickup_date)) / 86400
252
+ #service_codes_to_delivery_dates[ "UPS 12"+ service_code + " Transit Days"] = delivery_date
253
+ end
254
+ end
255
+ delivery_date
256
+ #response
257
+ end
258
+
259
+ def response_to_price(response)
260
+ #puts response
261
+ response_doc = REXML::Document.new(response)
262
+ response_code = response_doc.elements['//ResponseStatusCode'].text.to_i
263
+ raise "Invalid response from ups:\n#{response_doc.to_s}" if(!response_code || response_code != 1)
264
+ delivery_rate = 0
265
+
266
+ delivery_rate = response_doc.elements['//RatedShipment/TotalCharges/MonetaryValue'].text.to_f
267
+
268
+ delivery_rate
269
+ #response
270
+ end
271
+
272
+ def self.state_from_zip(zip)
273
+ zip = zip.to_i
274
+ {
275
+ (99500...99929) => "AK",
276
+ (35000...36999) => "AL",
277
+ (71600...72999) => "AR",
278
+ (75502...75505) => "AR",
279
+ (85000...86599) => "AZ",
280
+ (90000...96199) => "CA",
281
+ (80000...81699) => "CO",
282
+ (6000...6999) => "CT",
283
+ (20000...20099) => "DC",
284
+ (20200...20599) => "DC",
285
+ (19700...19999) => "DE",
286
+ (32000...33999) => "FL",
287
+ (34100...34999) => "FL",
288
+ (30000...31999) => "GA",
289
+ (96700...96798) => "HI",
290
+ (96800...96899) => "HI",
291
+ (50000...52999) => "IA",
292
+ (83200...83899) => "ID",
293
+ (60000...62999) => "IL",
294
+ (46000...47999) => "IN",
295
+ (66000...67999) => "KS",
296
+ (40000...42799) => "KY",
297
+ (45275...45275) => "KY",
298
+ (70000...71499) => "LA",
299
+ (71749...71749) => "LA",
300
+ (1000...2799) => "MA",
301
+ (20331...20331) => "MD",
302
+ (20600...21999) => "MD",
303
+ (3801...3801) => "ME",
304
+ (3804...3804) => "ME",
305
+ (3900...4999) => "ME",
306
+ (48000...49999) => "MI",
307
+ (55000...56799) => "MN",
308
+ (63000...65899) => "MO",
309
+ (38600...39799) => "MS",
310
+ (59000...59999) => "MT",
311
+ (27000...28999) => "NC",
312
+ (58000...58899) => "ND",
313
+ (68000...69399) => "NE",
314
+ (3000...3803) => "NH",
315
+ (3809...3899) => "NH",
316
+ (7000...8999) => "NJ",
317
+ (87000...88499) => "NM",
318
+ (89000...89899) => "NV",
319
+ (400...599) => "NY",
320
+ (6390...6390) => "NY",
321
+ (9000...14999) => "NY",
322
+ (43000...45999) => "OH",
323
+ (73000...73199) => "OK",
324
+ (73400...74999) => "OK",
325
+ (97000...97999) => "OR",
326
+ (15000...19699) => "PA",
327
+ (2800...2999) => "RI",
328
+ (6379...6379) => "RI",
329
+ (29000...29999) => "SC",
330
+ (57000...57799) => "SD",
331
+ (37000...38599) => "TN",
332
+ (72395...72395) => "TN",
333
+ (73300...73399) => "TX",
334
+ (73949...73949) => "TX",
335
+ (75000...79999) => "TX",
336
+ (88501...88599) => "TX",
337
+ (84000...84799) => "UT",
338
+ (20105...20199) => "VA",
339
+ (20301...20301) => "VA",
340
+ (20370...20370) => "VA",
341
+ (22000...24699) => "VA",
342
+ (5000...5999) => "VT",
343
+ (98000...99499) => "WA",
344
+ (49936...49936) => "WI",
345
+ (53000...54999) => "WI",
346
+ (24700...26899) => "WV",
347
+ (82000...83199) => "WY"
348
+ }.each do |range, state|
349
+ return state if range.include? zip
350
+ end
351
+
352
+ raise ShippingError, "Invalid zip code"
353
+ end
354
+
355
+
356
+ end
357
+ end
metadata ADDED
@@ -0,0 +1,119 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ShippingInfo
3
+ version: !ruby/object:Gem::Version
4
+ version: '2.0'
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Piyush Patel
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-10-05 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: httparty
16
+ requirement: &3108444 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 0.8.0
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *3108444
25
+ - !ruby/object:Gem::Dependency
26
+ name: nokogiri
27
+ requirement: &3107772 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ~>
31
+ - !ruby/object:Gem::Version
32
+ version: 1.5.0
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *3107772
36
+ - !ruby/object:Gem::Dependency
37
+ name: rspec
38
+ requirement: &3107136 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ~>
42
+ - !ruby/object:Gem::Version
43
+ version: 2.9.0
44
+ type: :development
45
+ prerelease: false
46
+ version_requirements: *3107136
47
+ - !ruby/object:Gem::Dependency
48
+ name: vcr
49
+ requirement: &3106620 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: 2.0.0
55
+ type: :development
56
+ prerelease: false
57
+ version_requirements: *3106620
58
+ - !ruby/object:Gem::Dependency
59
+ name: fakeweb
60
+ requirement: &3105912 !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ! '>='
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ type: :development
67
+ prerelease: false
68
+ version_requirements: *3105912
69
+ description: Provides an interface to get shipping rates and transit time for Fedex,
70
+ UPS, and USPS
71
+ email:
72
+ - er.piyushpatel@gmail.com
73
+ executables: []
74
+ extensions: []
75
+ extra_rdoc_files: []
76
+ files:
77
+ - lib/fedex/address.rb
78
+ - lib/fedex/credentials.rb
79
+ - lib/fedex/helpers.rb
80
+ - lib/fedex/label.rb
81
+ - lib/fedex/rate.rb
82
+ - lib/fedex/request/address.rb
83
+ - lib/fedex/request/base.rb
84
+ - lib/fedex/request/label.rb
85
+ - lib/fedex/request/rate.rb
86
+ - lib/fedex/request/shipment.rb
87
+ - lib/fedex/request/tracking_information.rb
88
+ - lib/fedex/shipment.rb
89
+ - lib/fedex/tracking_information/event.rb
90
+ - lib/fedex/tracking_information.rb
91
+ - lib/fedex/version.rb
92
+ - lib/fedex.rb
93
+ - lib/shipping.rb
94
+ - lib/ups/UpsInfo.rb
95
+ homepage: http://rubygems.org/gems/shippinginfo
96
+ licenses: []
97
+ post_install_message:
98
+ rdoc_options: []
99
+ require_paths:
100
+ - lib
101
+ required_ruby_version: !ruby/object:Gem::Requirement
102
+ none: false
103
+ requirements:
104
+ - - ! '>='
105
+ - !ruby/object:Gem::Version
106
+ version: '0'
107
+ required_rubygems_version: !ruby/object:Gem::Requirement
108
+ none: false
109
+ requirements:
110
+ - - ! '>='
111
+ - !ruby/object:Gem::Version
112
+ version: '0'
113
+ requirements: []
114
+ rubyforge_project: ShippingInfo
115
+ rubygems_version: 1.8.16
116
+ signing_key:
117
+ specification_version: 3
118
+ summary: Shipping Info Services
119
+ test_files: []