ideaoforder-shipping 1.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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