ideaoforder-shipping 1.6.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +429 -0
- data/README +51 -0
- data/Rakefile +251 -0
- data/lib/shipping/base.rb +314 -0
- data/lib/shipping/fedex.rb +585 -0
- data/lib/shipping/ups.rb +809 -0
- metadata +69 -0
data/lib/shipping/ups.rb
ADDED
@@ -0,0 +1,809 @@
|
|
1
|
+
# Author:: Lucas Carlson (mailto:lucas@rufy.com)
|
2
|
+
# Copyright:: Copyright (c) 2005 Lucas Carlson
|
3
|
+
# License:: LGPL
|
4
|
+
|
5
|
+
# Updated:: 12-22-2008 by Mark Dickson (mailto:mark@sitesteaders.com)
|
6
|
+
|
7
|
+
module Shipping
|
8
|
+
|
9
|
+
class UPS < Base
|
10
|
+
include REXML
|
11
|
+
API_VERSION = "1.0001"
|
12
|
+
|
13
|
+
# For current implementation (XML) docs, see http://www.ups.com/gec/techdocs/pdf/dtk_RateXML_V1.zip
|
14
|
+
def price
|
15
|
+
@required = [:zip, :country, :sender_zip, :weight]
|
16
|
+
@required += [:ups_license_number, :ups_user, :ups_password]
|
17
|
+
|
18
|
+
@insured_value ||= 0
|
19
|
+
@country ||= 'US'
|
20
|
+
@sender_country ||= 'US'
|
21
|
+
@service_type ||= 'ground' # default to UPS ground
|
22
|
+
@ups_url ||= "https://wwwcie.ups.com/ups.app/xml"
|
23
|
+
@ups_tool = '/Rate'
|
24
|
+
|
25
|
+
state = STATES.has_value?(@state.downcase) ? STATES.index(@state.downcase).upcase : @state.upcase unless @state.blank?
|
26
|
+
sender_state = STATES.has_value?(@sender_state.downcase) ? STATES.index(@sender_state.downcase).upcase : @sender_state.upcase unless @sender_state.blank?
|
27
|
+
|
28
|
+
# With UPS need to send two xmls
|
29
|
+
# First one to authenticate, second for the request
|
30
|
+
b = request_access
|
31
|
+
b.instruct!
|
32
|
+
|
33
|
+
b.RatingServiceSelectionRequest { |b|
|
34
|
+
b.Request { |b|
|
35
|
+
b.TransactionReference { |b|
|
36
|
+
b.CustomerContext 'Rating and Service'
|
37
|
+
b.XpciVersion API_VERSION
|
38
|
+
}
|
39
|
+
b.RequestAction 'Rate'
|
40
|
+
}
|
41
|
+
b.CustomerClassification { |b|
|
42
|
+
b.Code CustomerTypes[@customer_type] || '01'
|
43
|
+
}
|
44
|
+
b.PickupType { |b|
|
45
|
+
b.Code @pickup_type || '01'
|
46
|
+
}
|
47
|
+
b.Shipment { |b|
|
48
|
+
b.Shipper { |b|
|
49
|
+
b.Address { |b|
|
50
|
+
b.PostalCode @sender_zip
|
51
|
+
b.CountryCode @sender_country unless @sender_country.blank?
|
52
|
+
b.City @sender_city unless @sender_city.blank?
|
53
|
+
b.StateProvinceCode sender_state unless sender_state.blank?
|
54
|
+
}
|
55
|
+
}
|
56
|
+
b.ShipTo { |b|
|
57
|
+
b.Address { |b|
|
58
|
+
b.PostalCode @zip
|
59
|
+
b.CountryCode @country unless @country.blank?
|
60
|
+
b.City @city unless @city.blank?
|
61
|
+
b.StateProvinceCode state unless state.blank?
|
62
|
+
unless @commercial
|
63
|
+
b.ResidentialAddressIndicator
|
64
|
+
end
|
65
|
+
}
|
66
|
+
}
|
67
|
+
b.Service { |b| # The service code
|
68
|
+
b.Code ServiceTypes[@service_type] || '03' # defaults to ground
|
69
|
+
}
|
70
|
+
b.Package { |b| # Package Details
|
71
|
+
b.PackagingType { |b|
|
72
|
+
b.Code PackageTypes[@packaging_type] || '02' # defaults to 'your packaging'
|
73
|
+
b.Description 'Package'
|
74
|
+
}
|
75
|
+
b.Description 'Rate Shopping'
|
76
|
+
b.PackageWeight { |b|
|
77
|
+
b.Weight @weight
|
78
|
+
b.UnitOfMeasurement { |b|
|
79
|
+
b.Code @weight_units || 'LBS' # or KGS
|
80
|
+
}
|
81
|
+
}
|
82
|
+
b.Dimensions { |b|
|
83
|
+
b.UnitOfMeasurement { |b|
|
84
|
+
b.Code @measure_units || 'IN'
|
85
|
+
}
|
86
|
+
b.Length @measure_length || 0
|
87
|
+
b.Width @measure_width || 0
|
88
|
+
b.Height @measure_height || 0
|
89
|
+
}
|
90
|
+
b.PackageServiceOptions { |b|
|
91
|
+
b.InsuredValue { |b|
|
92
|
+
b.CurrencyCode @currency_code || 'US'
|
93
|
+
b.MonetaryValue @insured_value
|
94
|
+
}
|
95
|
+
}
|
96
|
+
}
|
97
|
+
}
|
98
|
+
}
|
99
|
+
|
100
|
+
get_response @ups_url + @ups_tool
|
101
|
+
|
102
|
+
return REXML::XPath.first(@response, "//RatingServiceSelectionResponse/RatedShipment/TransportationCharges/MonetaryValue").text.to_f
|
103
|
+
rescue
|
104
|
+
raise ShippingError, get_error
|
105
|
+
end
|
106
|
+
|
107
|
+
def validated_price
|
108
|
+
@required = [:zip, :country, :sender_zip, :weight]
|
109
|
+
@required += [:ups_license_number, :ups_user, :ups_password]
|
110
|
+
|
111
|
+
@insured_value ||= 0
|
112
|
+
@country ||= 'US'
|
113
|
+
@sender_country ||= 'US'
|
114
|
+
@service_type ||= 'ground' # default to UPS ground
|
115
|
+
@ups_url ||= "https://wwwcie.ups.com/ups.app/xml"
|
116
|
+
@ups_tool = '/Rate'
|
117
|
+
|
118
|
+
state = STATES.has_value?(@state.downcase) ? STATES.index(@state.downcase).upcase : @state.upcase unless @state.blank?
|
119
|
+
sender_state = STATES.has_value?(@sender_state.downcase) ? STATES.index(@sender_state.downcase).upcase : @sender_state.upcase unless @sender_state.blank?
|
120
|
+
|
121
|
+
# With UPS need to send two xmls
|
122
|
+
# First one to authenticate, second for the request
|
123
|
+
b = request_access
|
124
|
+
b.instruct!
|
125
|
+
|
126
|
+
b.RatingServiceSelectionRequest { |b|
|
127
|
+
b.Request { |b|
|
128
|
+
b.TransactionReference { |b|
|
129
|
+
b.CustomerContext 'Rating and Service'
|
130
|
+
b.XpciVersion API_VERSION
|
131
|
+
}
|
132
|
+
b.RequestAction 'Rate'
|
133
|
+
}
|
134
|
+
b.CustomerClassification { |b|
|
135
|
+
b.Code CustomerTypes[@customer_type] || '01'
|
136
|
+
}
|
137
|
+
b.PickupType { |b|
|
138
|
+
b.Code @pickup_type || '01'
|
139
|
+
}
|
140
|
+
b.Shipment { |b|
|
141
|
+
b.Shipper { |b|
|
142
|
+
b.Address { |b|
|
143
|
+
b.PostalCode @sender_zip
|
144
|
+
b.CountryCode @sender_country unless @sender_country.blank?
|
145
|
+
b.City @sender_city unless @sender_city.blank?
|
146
|
+
b.StateProvinceCode sender_state unless sender_state.blank?
|
147
|
+
}
|
148
|
+
}
|
149
|
+
b.ShipTo { |b|
|
150
|
+
b.Address { |b|
|
151
|
+
b.PostalCode @zip
|
152
|
+
b.CountryCode @country unless @country.blank?
|
153
|
+
b.City @city unless @city.blank?
|
154
|
+
b.StateProvinceCode state unless state.blank?
|
155
|
+
unless @commercial
|
156
|
+
b.ResidentialAddressIndicator
|
157
|
+
end
|
158
|
+
}
|
159
|
+
}
|
160
|
+
b.Service { |b| # The service code
|
161
|
+
b.Code ServiceTypes[@service_type] || '03' # defaults to ground
|
162
|
+
}
|
163
|
+
b.Package { |b| # Package Details
|
164
|
+
b.PackagingType { |b|
|
165
|
+
b.Code PackageTypes[@packaging_type] || '02' # defaults to 'your packaging'
|
166
|
+
b.Description 'Package'
|
167
|
+
}
|
168
|
+
b.Description 'Rate Shopping'
|
169
|
+
b.PackageWeight { |b|
|
170
|
+
b.Weight @weight
|
171
|
+
b.UnitOfMeasurement { |b|
|
172
|
+
b.Code @weight_units || 'LBS' # or KGS
|
173
|
+
}
|
174
|
+
}
|
175
|
+
b.Dimensions { |b|
|
176
|
+
b.UnitOfMeasurement { |b|
|
177
|
+
b.Code @measure_units || 'IN'
|
178
|
+
}
|
179
|
+
b.Length @measure_length || 0
|
180
|
+
b.Width @measure_width || 0
|
181
|
+
b.Height @measure_height || 0
|
182
|
+
}
|
183
|
+
b.PackageServiceOptions { |b|
|
184
|
+
b.InsuredValue { |b|
|
185
|
+
b.CurrencyCode @currency_code || 'US'
|
186
|
+
b.MonetaryValue @insured_value
|
187
|
+
}
|
188
|
+
}
|
189
|
+
}
|
190
|
+
}
|
191
|
+
}
|
192
|
+
|
193
|
+
get_response @ups_url + @ups_tool
|
194
|
+
|
195
|
+
status = XPath.first(@response, "//RatingServiceSelectionResponse/Response/ResponseStatusCode").text.to_i
|
196
|
+
if status == 1
|
197
|
+
return XPath.first(@response, "//RatingServiceSelectionResponse/RatedShipment/TransportationCharges/MonetaryValue").text.to_f
|
198
|
+
else
|
199
|
+
return XPath.first(@response, "//RatingServiceSelectionResponse/Response/Error/ErrorDescription").text
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
def price_multiple
|
204
|
+
return_boxes = Hash.new
|
205
|
+
return_boxes[:method] = 'UPS' + ' ' + @service_type.split("_").each{|word| word.capitalize}.join(" ")
|
206
|
+
return_boxes[:packages] = Array.new
|
207
|
+
total_cost = 0
|
208
|
+
if @max_quantity == nil && @weight != nil
|
209
|
+
cost = self.validated_price
|
210
|
+
if cost.is_a? String
|
211
|
+
return {:errors => cost}
|
212
|
+
else
|
213
|
+
return {:cost => cost}
|
214
|
+
end
|
215
|
+
exit
|
216
|
+
end
|
217
|
+
boxes = self.boxes
|
218
|
+
self.weight = boxes.first[:weight]
|
219
|
+
cost = self.validated_price
|
220
|
+
if cost.is_a? String
|
221
|
+
return {:errors => cost}
|
222
|
+
else
|
223
|
+
for i in 1..(boxes.length - 1)
|
224
|
+
return_boxes[:packages] << {
|
225
|
+
:cost => cost,
|
226
|
+
:weight => boxes.first[:weight],
|
227
|
+
:quantity => boxes.first[:quantity],
|
228
|
+
}
|
229
|
+
total_cost += cost.to_d
|
230
|
+
end
|
231
|
+
self.weight = boxes.last[:weight]
|
232
|
+
cost = self.validated_price
|
233
|
+
return_boxes[:packages] << {
|
234
|
+
:cost => cost,
|
235
|
+
:weight => boxes.last[:weight],
|
236
|
+
:quantity => boxes.last[:quantity],
|
237
|
+
}
|
238
|
+
total_cost += cost.to_d
|
239
|
+
return_boxes[:cost] = total_cost
|
240
|
+
return_boxes[:num_packages] = return_boxes[:packages].length
|
241
|
+
return return_boxes
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
def rates_multiple
|
246
|
+
return_boxes = Hash.new
|
247
|
+
return_boxes[:packages] = Array.new
|
248
|
+
boxes = self.boxes
|
249
|
+
|
250
|
+
# we only look at the first and last boxes--the others will all be the same
|
251
|
+
self.weight = boxes.first[:weight]
|
252
|
+
rates = self.rates
|
253
|
+
if rates.is_a? String
|
254
|
+
return {:errors => rates}
|
255
|
+
else
|
256
|
+
for i in 1..(boxes.length - 1)
|
257
|
+
return_boxes[:packages] << rates
|
258
|
+
end
|
259
|
+
total_rates = rates.each {|code, service| service[:price] *= (boxes.length - 1); service[:billing_weight] *= (boxes.length - 1)}
|
260
|
+
self.weight = boxes.last[:weight]
|
261
|
+
rates = self.rates
|
262
|
+
return_boxes[:packages] << rates
|
263
|
+
return_boxes[:rates] = total_rates.each {|code, service| service[:price] += (rates[code][:price]); service[:billing_weight] += rates[code][:billing_weight]}
|
264
|
+
|
265
|
+
return return_boxes
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
def rates
|
270
|
+
@required = [:zip, :country, :sender_zip, :weight]
|
271
|
+
@required += [:ups_license_number, :ups_user, :ups_password]
|
272
|
+
|
273
|
+
@insured_value ||= 0
|
274
|
+
@country ||= 'US'
|
275
|
+
@sender_country ||= 'US'
|
276
|
+
@ups_url ||= "https://wwwcie.ups.com/ups.app/xml"
|
277
|
+
@ups_tool = '/Rate'
|
278
|
+
|
279
|
+
state = STATES.has_value?(@state.downcase) ? STATES.index(@state.downcase).upcase : @state.upcase unless @state.blank?
|
280
|
+
sender_state = STATES.has_value?(@sender_state.downcase) ? STATES.index(@sender_state.downcase).upcase : @sender_state.upcase unless @sender_state.blank?
|
281
|
+
|
282
|
+
# With UPS need to send two xmls
|
283
|
+
# First one to authenticate, second for the request
|
284
|
+
b = request_access
|
285
|
+
b.instruct!
|
286
|
+
|
287
|
+
b.RatingServiceSelectionRequest { |b|
|
288
|
+
b.Request { |b|
|
289
|
+
b.TransactionReference { |b|
|
290
|
+
b.CustomerContext 'Rating and Service'
|
291
|
+
b.XpciVersion API_VERSION
|
292
|
+
}
|
293
|
+
b.RequestAction 'Rate'
|
294
|
+
b.RequestOption 'Shop'
|
295
|
+
}
|
296
|
+
b.CustomerClassification { |b|
|
297
|
+
b.Code CustomerTypes[@customer_type] || '01'
|
298
|
+
}
|
299
|
+
b.PickupType { |b|
|
300
|
+
b.Code @pickup_type || '01'
|
301
|
+
}
|
302
|
+
b.Shipment { |b|
|
303
|
+
b.Shipper { |b|
|
304
|
+
b.Address { |b|
|
305
|
+
b.PostalCode @sender_zip
|
306
|
+
b.CountryCode @sender_country unless @sender_country.blank?
|
307
|
+
b.City @sender_city unless @sender_city.blank?
|
308
|
+
b.StateProvinceCode sender_state unless sender_state.blank?
|
309
|
+
}
|
310
|
+
}
|
311
|
+
b.ShipTo { |b|
|
312
|
+
b.Address { |b|
|
313
|
+
b.PostalCode @zip
|
314
|
+
b.CountryCode @country unless @country.blank?
|
315
|
+
b.City @city unless @city.blank?
|
316
|
+
b.StateProvinceCode state unless state.blank?
|
317
|
+
unless @commercial
|
318
|
+
b.ResidentialAddressIndicator
|
319
|
+
end
|
320
|
+
}
|
321
|
+
}
|
322
|
+
b.Package { |b| # Package Details
|
323
|
+
b.PackagingType { |b|
|
324
|
+
b.Code PackageTypes[@packaging_type] || '02' # defaults to 'your packaging'
|
325
|
+
b.Description 'Package'
|
326
|
+
}
|
327
|
+
b.Description 'Rate Shopping'
|
328
|
+
b.PackageWeight { |b|
|
329
|
+
b.Weight @weight
|
330
|
+
b.UnitOfMeasurement { |b|
|
331
|
+
b.Code @weight_units || 'LBS' # or KGS
|
332
|
+
}
|
333
|
+
}
|
334
|
+
b.Dimensions { |b|
|
335
|
+
b.UnitOfMeasurement { |b|
|
336
|
+
b.Code @measure_units || 'IN'
|
337
|
+
}
|
338
|
+
b.Length @measure_length || 0
|
339
|
+
b.Width @measure_width || 0
|
340
|
+
b.Height @measure_height || 0
|
341
|
+
}
|
342
|
+
b.PackageServiceOptions { |b|
|
343
|
+
b.InsuredValue { |b|
|
344
|
+
b.CurrencyCode @currency_code || 'US'
|
345
|
+
b.MonetaryValue @insured_value
|
346
|
+
}
|
347
|
+
}
|
348
|
+
}
|
349
|
+
}
|
350
|
+
}
|
351
|
+
|
352
|
+
get_response @ups_url + @ups_tool
|
353
|
+
|
354
|
+
status = XPath.first(@response, "//RatingServiceSelectionResponse/Response/ResponseStatusCode").text.to_i
|
355
|
+
if status == 1
|
356
|
+
shipmethods = Hash.new
|
357
|
+
@response.elements.each('//RatedShipment') do |shipmethod|
|
358
|
+
index = XPath.first(shipmethod, "Service/Code").text
|
359
|
+
shipmethods[index.to_i] = {
|
360
|
+
:service => ServiceTypes.index(index),
|
361
|
+
:service_name => ServiceTypes.index(index).split("_").each{|word| word.capitalize!}.join(" "),
|
362
|
+
:price => XPath.first(shipmethod, "TransportationCharges/MonetaryValue").text.to_f,
|
363
|
+
:currency => XPath.first(shipmethod, "TransportationCharges/CurrencyCode").text,
|
364
|
+
:billing_weight => XPath.first(shipmethod, "BillingWeight/Weight").text.to_f,
|
365
|
+
:weight_units => XPath.first(shipmethod, "BillingWeight/UnitOfMeasurement/Code").text,
|
366
|
+
}
|
367
|
+
end
|
368
|
+
return shipmethods
|
369
|
+
else
|
370
|
+
return XPath.first(@response, "//RatingServiceSelectionResponse/Response/Error/ErrorDescription").text
|
371
|
+
end
|
372
|
+
end
|
373
|
+
|
374
|
+
# See http://www.ups.com/gec/techdocs/pdf/dtk_AddrValidateXML_V1.zip for API info
|
375
|
+
def valid_address?( delta = 1.0 )
|
376
|
+
@required = [:ups_license_number, :ups_user, :ups_password]
|
377
|
+
@ups_url ||= "https://wwwcie.ups.com/ups.app/xml"
|
378
|
+
@ups_tool = '/AV'
|
379
|
+
|
380
|
+
state = nil
|
381
|
+
if @state:
|
382
|
+
state = STATES.has_value?(@state.downcase) ? STATES.index(@state.downcase) : @state
|
383
|
+
end
|
384
|
+
|
385
|
+
b = request_access
|
386
|
+
b.instruct!
|
387
|
+
|
388
|
+
b.AddressValidationRequest {|b|
|
389
|
+
b.Request {|b|
|
390
|
+
b.RequestAction "AV"
|
391
|
+
b.TransactionReference {|b|
|
392
|
+
b.CustomerContext "#{@city}, #{state} #{@zip}"
|
393
|
+
b.XpciVersion API_VERSION
|
394
|
+
}
|
395
|
+
}
|
396
|
+
b.Address {|b|
|
397
|
+
b.City @city
|
398
|
+
b.StateProvinceCode state
|
399
|
+
b.PostalCode @zip
|
400
|
+
}
|
401
|
+
}
|
402
|
+
|
403
|
+
get_response @ups_url + @ups_tool
|
404
|
+
|
405
|
+
if REXML::XPath.first(@response, "//AddressValidationResponse/Response/ResponseStatusCode").text == "1" && REXML::XPath.first(@response, "//AddressValidationResponse/AddressValidationResult/Quality").text.to_f >= delta
|
406
|
+
return true
|
407
|
+
else
|
408
|
+
return false
|
409
|
+
end
|
410
|
+
rescue ShippingError
|
411
|
+
raise ShippingError, get_error
|
412
|
+
end
|
413
|
+
|
414
|
+
# See Ship-WW-XML.pdf for API info
|
415
|
+
# @image_type = [GIF|EPL]
|
416
|
+
def label
|
417
|
+
@required = [:ups_license_number, :ups_shipper_number, :ups_user, :ups_password]
|
418
|
+
@required += [:phone, :email, :company, :address, :city, :state, :zip]
|
419
|
+
@required += [:sender_phone, :sender_email, :sender_company, :sender_address, :sender_city, :sender_state, :sender_zip ]
|
420
|
+
@ups_url ||= "https://wwwcie.ups.com/ups.app/xml"
|
421
|
+
@ups_tool = '/ShipConfirm'
|
422
|
+
|
423
|
+
state = STATES.has_value?(@state.downcase) ? STATES.index(@state.downcase).upcase : @state.upcase unless @state.blank?
|
424
|
+
sender_state = STATES.has_value?(@sender_state.downcase) ? STATES.index(@sender_state.downcase).upcase : @sender_state.upcase unless @sender_state.blank?
|
425
|
+
|
426
|
+
# make ConfirmRequest and get Confirm Response
|
427
|
+
b = request_access
|
428
|
+
b.instruct!
|
429
|
+
|
430
|
+
b.ShipmentConfirmRequest { |b|
|
431
|
+
b.Request { |b|
|
432
|
+
b.RequestAction "ShipConfirm"
|
433
|
+
b.RequestOption "nonvalidate"
|
434
|
+
b.TransactionReference { |b|
|
435
|
+
b.CustomerContext "#{@city}, #{state} #{@zip}"
|
436
|
+
b.XpciVersion API_VERSION
|
437
|
+
}
|
438
|
+
}
|
439
|
+
b.Shipment { |b|
|
440
|
+
b.Shipper { |b|
|
441
|
+
b.ShipperNumber @ups_shipper_number
|
442
|
+
b.Name @sender_name
|
443
|
+
b.Address { |b|
|
444
|
+
b.AddressLine1 @sender_address unless @sender_address.blank?
|
445
|
+
b.PostalCode @sender_zip
|
446
|
+
b.CountryCode @sender_country unless @sender_country.blank?
|
447
|
+
b.City @sender_city unless @sender_city.blank?
|
448
|
+
b.StateProvinceCode sender_state unless sender_state.blank?
|
449
|
+
}
|
450
|
+
}
|
451
|
+
b.ShipFrom { |b|
|
452
|
+
b.CompanyName @sender_company
|
453
|
+
b.Address { |b|
|
454
|
+
b.AddressLine1 @sender_address unless @sender_address.blank?
|
455
|
+
b.PostalCode @sender_zip
|
456
|
+
b.CountryCode @sender_country unless @sender_country.blank?
|
457
|
+
b.City @sender_city unless @sender_city.blank?
|
458
|
+
b.StateProvinceCode sender_state unless sender_state.blank?
|
459
|
+
}
|
460
|
+
}
|
461
|
+
b.ShipTo { |b|
|
462
|
+
b.CompanyName @company
|
463
|
+
b.Address { |b|
|
464
|
+
b.AddressLine1 @address unless @address.blank?
|
465
|
+
b.PostalCode @zip
|
466
|
+
b.CountryCode @country unless @country.blank?
|
467
|
+
b.City @city unless @city.blank?
|
468
|
+
b.StateProvinceCode state unless state.blank?
|
469
|
+
unless @commercial
|
470
|
+
b.ResidentialAddressIndicator
|
471
|
+
end
|
472
|
+
}
|
473
|
+
}
|
474
|
+
b.PaymentInformation { |b|
|
475
|
+
pay_type = PaymentTypes[@pay_type] || 'Prepaid'
|
476
|
+
|
477
|
+
if pay_type == 'Prepaid'
|
478
|
+
b.Prepaid { |b|
|
479
|
+
b.BillShipper { |b|
|
480
|
+
b.AccountNumber @ups_shipper_number
|
481
|
+
}
|
482
|
+
}
|
483
|
+
elsif pay_type == 'BillThirdParty'
|
484
|
+
b.BillThirdParty { |b|
|
485
|
+
b.BillThirdPartyShipper { |b|
|
486
|
+
b.AccountNumber @billing_account
|
487
|
+
b.ThirdParty { |b|
|
488
|
+
b.Address { |b|
|
489
|
+
b.PostalCode @billing_zip
|
490
|
+
b.CountryCode @billing_country
|
491
|
+
}
|
492
|
+
}
|
493
|
+
}
|
494
|
+
}
|
495
|
+
elsif pay_type == 'FreightCollect'
|
496
|
+
b.FreightCollect { |b|
|
497
|
+
b.BillReceiver { |b|
|
498
|
+
b.AccountNumber @billing_account
|
499
|
+
}
|
500
|
+
}
|
501
|
+
else
|
502
|
+
raise ShippingError, "Valid pay_types are 'prepaid', 'bill_third_party', or 'freight_collect'."
|
503
|
+
end
|
504
|
+
}
|
505
|
+
b.Service { |b| # The service code
|
506
|
+
b.Code ServiceTypes[@service_type] || '03' # defaults to ground
|
507
|
+
}
|
508
|
+
b.Package { |b| # Package Details
|
509
|
+
b.PackagingType { |b|
|
510
|
+
b.Code PackageTypes[@packaging_type] || '02' # defaults to 'your packaging'
|
511
|
+
b.Description 'Package'
|
512
|
+
}
|
513
|
+
b.PackageWeight { |b|
|
514
|
+
b.Weight @weight
|
515
|
+
b.UnitOfMeasurement { |b|
|
516
|
+
b.Code @weight_units || 'LBS' # or KGS
|
517
|
+
}
|
518
|
+
}
|
519
|
+
b.Dimensions { |b|
|
520
|
+
b.UnitOfMeasurement { |b|
|
521
|
+
b.Code @measure_units || 'IN'
|
522
|
+
}
|
523
|
+
b.Length @measure_length || 0
|
524
|
+
b.Width @measure_width || 0
|
525
|
+
b.Height @measure_height || 0
|
526
|
+
} if @measure_length || @measure_width || @measure_height
|
527
|
+
b.PackageServiceOptions { |b|
|
528
|
+
b.InsuredValue { |b|
|
529
|
+
b.CurrencyCode @currency_code || 'US'
|
530
|
+
b.MonetaryValue @insured_value
|
531
|
+
}
|
532
|
+
} if @insured_value
|
533
|
+
}
|
534
|
+
}
|
535
|
+
b.LabelSpecification { |b|
|
536
|
+
image_type = @image_type || 'GIF' # default to GIF
|
537
|
+
|
538
|
+
b.LabelPrintMethod { |b|
|
539
|
+
b.Code image_type
|
540
|
+
}
|
541
|
+
if image_type == 'GIF'
|
542
|
+
b.HTTPUserAgent 'Mozilla/5.0'
|
543
|
+
b.LabelImageFormat { |b|
|
544
|
+
b.Code 'GIF'
|
545
|
+
}
|
546
|
+
elsif image_type == 'EPL'
|
547
|
+
b.LabelStockSize { |b|
|
548
|
+
b.Height '4'
|
549
|
+
b.Width '6'
|
550
|
+
}
|
551
|
+
else
|
552
|
+
raise ShippingError, "Valid image_types are 'EPL' or 'GIF'."
|
553
|
+
end
|
554
|
+
}
|
555
|
+
}
|
556
|
+
|
557
|
+
# get ConfirmResponse
|
558
|
+
get_response @ups_url + @ups_tool
|
559
|
+
begin
|
560
|
+
shipment_digest = REXML::XPath.first(@response, '//ShipmentConfirmResponse/ShipmentDigest').text
|
561
|
+
rescue
|
562
|
+
raise ShippingError, get_error
|
563
|
+
end
|
564
|
+
|
565
|
+
# make AcceptRequest and get AcceptResponse
|
566
|
+
@ups_tool = '/ShipAccept'
|
567
|
+
|
568
|
+
b = request_access
|
569
|
+
b.instruct!
|
570
|
+
|
571
|
+
b.ShipmentAcceptRequest { |b|
|
572
|
+
b.Request { |b|
|
573
|
+
b.RequestAction "ShipAccept"
|
574
|
+
b.TransactionReference { |b|
|
575
|
+
b.CustomerContext "#{@city}, #{state} #{@zip}"
|
576
|
+
b.XpciVersion API_VERSION
|
577
|
+
}
|
578
|
+
}
|
579
|
+
b.ShipmentDigest shipment_digest
|
580
|
+
}
|
581
|
+
|
582
|
+
# get AcceptResponse
|
583
|
+
get_response @ups_url + @ups_tool
|
584
|
+
|
585
|
+
begin
|
586
|
+
response = Hash.new
|
587
|
+
response[:tracking_number] = REXML::XPath.first(@response, "//ShipmentAcceptResponse/ShipmentResults/PackageResults/TrackingNumber").text
|
588
|
+
response[:encoded_image] = REXML::XPath.first(@response, "//ShipmentAcceptResponse/ShipmentResults/PackageResults/LabelImage/GraphicImage").text
|
589
|
+
response[:image] = Tempfile.new("shipping_label")
|
590
|
+
response[:image].write Base64.decode64( response[:encoded_image] )
|
591
|
+
response[:image].rewind
|
592
|
+
rescue
|
593
|
+
raise ShippingError, get_error
|
594
|
+
end
|
595
|
+
|
596
|
+
# allows for things like fedex.label.url
|
597
|
+
def response.method_missing(name, *args)
|
598
|
+
has_key?(name) ? self[name] : super
|
599
|
+
end
|
600
|
+
|
601
|
+
# don't allow people to edit the response
|
602
|
+
response.freeze
|
603
|
+
end
|
604
|
+
|
605
|
+
def void(tracking_number)
|
606
|
+
@required = [:ups_license_number, :ups_shipper_number, :ups_user, :ups_password]
|
607
|
+
@ups_url ||= "https://wwwcie.ups.com/ups.app/xml"
|
608
|
+
@ups_tool = '/Void'
|
609
|
+
|
610
|
+
# make ConfirmRequest and get Confirm Response
|
611
|
+
b = request_access
|
612
|
+
b.instruct!
|
613
|
+
|
614
|
+
b.VoidShipmentRequest { |b|
|
615
|
+
b.Request { |b|
|
616
|
+
b.RequestAction "Void"
|
617
|
+
b.TransactionReference { |b|
|
618
|
+
b.CustomerContext "Void #{@tracking_number}"
|
619
|
+
b.XpciVersion API_VERSION
|
620
|
+
}
|
621
|
+
}
|
622
|
+
b.ShipmentIdentificationNumber tracking_number
|
623
|
+
}
|
624
|
+
|
625
|
+
# get VoidResponse
|
626
|
+
get_response @ups_url + @ups_tool
|
627
|
+
status = REXML::XPath.first(@response, '//VoidShipmentResponse/Response/ResponseStatusCode').text
|
628
|
+
raise ShippingError, get_error if status == '0'
|
629
|
+
return true if status == '1'
|
630
|
+
end
|
631
|
+
|
632
|
+
# For current implementation (XML) docs, see http://www.ups.com/gec/techdocs/pdf/dtk_TimeNTransitXML_V1.zip
|
633
|
+
def transit_time
|
634
|
+
@required = [:zip, :sender_zip, :weight]
|
635
|
+
@required += [:ups_license_number, :ups_user, :ups_password]
|
636
|
+
|
637
|
+
@insured_value ||= 0
|
638
|
+
@country ||= 'US'
|
639
|
+
@sender_country ||= 'US'
|
640
|
+
@ups_url ||= "https://wwwcie.ups.com/ups.app/xml"
|
641
|
+
@ups_tool = '/TimeInTransit'
|
642
|
+
|
643
|
+
state = STATES.has_value?(@state.downcase) ? STATES.index(@state.downcase).upcase : @state.upcase unless @state.blank?
|
644
|
+
sender_state = STATES.has_value?(@sender_state.downcase) ? STATES.index(@sender_state.downcase).upcase : @sender_state.upcase unless @sender_state.blank?
|
645
|
+
|
646
|
+
# With UPS need to send two xmls
|
647
|
+
# First one to authenticate, second for the request
|
648
|
+
b = request_access
|
649
|
+
b.instruct!
|
650
|
+
|
651
|
+
b.TimeInTransitRequest { |b|
|
652
|
+
b.Request { |b|
|
653
|
+
b.RequestAction 'TimeInTransit'
|
654
|
+
b.TransactionReference { |b|
|
655
|
+
b.CustomerContext 'Time in Transit'
|
656
|
+
b.XpciVersion API_VERSION
|
657
|
+
}
|
658
|
+
}
|
659
|
+
b.TransitFrom { |b|
|
660
|
+
b.AddressArtifactFormat { |b|
|
661
|
+
#b.PoliticalDivision3 @sender_town unless @sender_town.blank? # for non-US towns
|
662
|
+
b.PoliticalDivision2 @sender_city unless @sender_city.blank?
|
663
|
+
b.PoliticalDivision1 sender_state unless sender_state.blank?
|
664
|
+
b.CountryCode @sender_country
|
665
|
+
b.PostCodePrimaryLow @sender_zip
|
666
|
+
}
|
667
|
+
}
|
668
|
+
b.TransitTo { |b|
|
669
|
+
b.AddressArtifactFormat { |b|
|
670
|
+
#b.PoliticalDivision3 @sender_town unless @sender_town.blank? # for non-US towns
|
671
|
+
b.PoliticalDivision2 @city unless @city.blank?
|
672
|
+
b.PoliticalDivision1 state unless state.blank?
|
673
|
+
b.CountryCode @country
|
674
|
+
b.PostCodePrimaryLow @zip
|
675
|
+
unless @commercial
|
676
|
+
b.ResidentialAddressIndicator
|
677
|
+
end
|
678
|
+
}
|
679
|
+
}
|
680
|
+
b.PickupDate @pickup_date || Time.now.strftime("%Y%m%d")
|
681
|
+
b.ShipmentWeight { |b|
|
682
|
+
b.Weight @weight
|
683
|
+
b.UnitOfMeasurement { |b|
|
684
|
+
b.Code @weight_units || 'LBS' # or KGS
|
685
|
+
}
|
686
|
+
}
|
687
|
+
b.TotalPackagesInShipment @packages || 1
|
688
|
+
b.InvoiceLineTotal { |b|
|
689
|
+
b.CurrencyCode @currency_code || 'US'
|
690
|
+
b.MonetaryValue @insured_value
|
691
|
+
}
|
692
|
+
}
|
693
|
+
|
694
|
+
get_response @ups_url + @ups_tool
|
695
|
+
|
696
|
+
status = XPath.first(@response, "//TimeInTransitResponse/Response/ResponseStatusCode").text.to_i
|
697
|
+
if status == 1
|
698
|
+
times = Hash.new
|
699
|
+
@response.elements.each('//ServiceSummary') do |shipmethod|
|
700
|
+
index = XPath.first(shipmethod, "Service/Code").text
|
701
|
+
times[index] = {
|
702
|
+
:service_name => XPath.first(shipmethod, "Service/Description").text,
|
703
|
+
:service => ServiceTimes.index(index),
|
704
|
+
:days => XPath.first(shipmethod, "EstimatedArrival/BusinessTransitDays").text.to_i,
|
705
|
+
:date => XPath.first(shipmethod, "EstimatedArrival/Date").text.to_date,
|
706
|
+
:time => XPath.first(shipmethod, "EstimatedArrival/Time").text,
|
707
|
+
}
|
708
|
+
end
|
709
|
+
return times
|
710
|
+
else
|
711
|
+
return XPath.first(@response, "//TimeInTransitResponse/Response/Error/ErrorDescription").text
|
712
|
+
end
|
713
|
+
end
|
714
|
+
|
715
|
+
private
|
716
|
+
|
717
|
+
def request_access
|
718
|
+
@data = String.new
|
719
|
+
b = Builder::XmlMarkup.new :target => @data
|
720
|
+
|
721
|
+
b.instruct!
|
722
|
+
b.AccessRequest {|b|
|
723
|
+
b.AccessLicenseNumber @ups_license_number
|
724
|
+
b.UserId @ups_user
|
725
|
+
b.Password @ups_password
|
726
|
+
}
|
727
|
+
return b
|
728
|
+
end
|
729
|
+
|
730
|
+
def get_error
|
731
|
+
return if @response.class != REXML::Document
|
732
|
+
|
733
|
+
error = REXML::XPath.first(@response, '//*/Response/Error')
|
734
|
+
return if !error
|
735
|
+
|
736
|
+
severity = REXML::XPath.first(error, '//ErrorSeverity').text
|
737
|
+
code = REXML::XPath.first(error, '//ErrorCode').text
|
738
|
+
description = REXML::XPath.first(error, '//ErrorDescription').text
|
739
|
+
begin
|
740
|
+
location = REXML::XPath.first(error, '//ErrorLocation/ErrorLocationElementName').text
|
741
|
+
rescue
|
742
|
+
location = 'unknown'
|
743
|
+
end
|
744
|
+
return "#{severity} Error ##{code} @ #{location}: #{description}"
|
745
|
+
end
|
746
|
+
|
747
|
+
# The following type hashes are to allow cross-api data retrieval
|
748
|
+
PackageTypes = {
|
749
|
+
"ups_envelope" => "01",
|
750
|
+
"your_packaging" => "02",
|
751
|
+
"ups_tube" => "03",
|
752
|
+
"ups_pak" => "04",
|
753
|
+
"ups_box" => "21",
|
754
|
+
"fedex_25_kg_box" => "24",
|
755
|
+
"fedex_10_kg_box" => "25"
|
756
|
+
}
|
757
|
+
|
758
|
+
ServiceTypes = {
|
759
|
+
"next_day" => "01",
|
760
|
+
"2day" => "02",
|
761
|
+
"ground_service" => "03",
|
762
|
+
"worldwide_express" => "07",
|
763
|
+
"worldwide_expedited" => "08",
|
764
|
+
"standard" => "11",
|
765
|
+
"3day" => "12",
|
766
|
+
"next_day_saver" => "13",
|
767
|
+
"next_day_early" => "14",
|
768
|
+
"worldwide_express_plus" => "54",
|
769
|
+
"2day_early" => "59"
|
770
|
+
}
|
771
|
+
|
772
|
+
ServiceTimes = {
|
773
|
+
"next_day" => "1DA",
|
774
|
+
"2day" => "2DA",
|
775
|
+
"ground_service" => "GND",
|
776
|
+
"worldwide_express" => "01",
|
777
|
+
"worldwide_expedited" => "05",
|
778
|
+
"standard" => "03",
|
779
|
+
"3day" => "3DS",
|
780
|
+
"next_day_saver" => "1DP",
|
781
|
+
"next_day_early" => "1DM",
|
782
|
+
"worldwide_express_plus" => "21",
|
783
|
+
"2day_early" => "2DM"
|
784
|
+
}
|
785
|
+
|
786
|
+
PickupTypes = {
|
787
|
+
'daily_pickup' => '01',
|
788
|
+
'customer_counter' => '03',
|
789
|
+
'one_time_pickup' => '06',
|
790
|
+
'on_call' => '07',
|
791
|
+
'suggested_retail_rates' => '11',
|
792
|
+
'letter_center' => '19',
|
793
|
+
'air_service_center' => '20'
|
794
|
+
}
|
795
|
+
|
796
|
+
CustomerTypes = {
|
797
|
+
'wholesale' => '01',
|
798
|
+
'ocassional' => '02',
|
799
|
+
'retail' => '04'
|
800
|
+
}
|
801
|
+
|
802
|
+
PaymentTypes = {
|
803
|
+
'prepaid' => 'Prepaid',
|
804
|
+
'consignee' => 'Consignee', # TODO: Implement
|
805
|
+
'bill_third_party' => 'BillThirdParty',
|
806
|
+
'freight_collect' => 'FreightCollect'
|
807
|
+
}
|
808
|
+
end
|
809
|
+
end
|