shipping 1.3.0 → 1.5.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.
data/lib/shipping/ups.rb CHANGED
@@ -2,87 +2,436 @@
2
2
  # Copyright:: Copyright (c) 2005 Lucas Carlson
3
3
  # License:: LGPL
4
4
 
5
- =begin
6
- UPS Service transaction_type
7
- ---------------------------------------------
8
- Next Day Air Early 1DM
9
- Next Day Air 1DA
10
- Next Day Air Intra 1DAPI (Puerto Rico)
11
- Next Day Air Saver 1DP
12
- 2nd Day Air A M2DM
13
- 2nd Day Air 2DA
14
- 3 Day Select 3DS
15
- Ground GND
16
- Canada Standard STD
17
- Worldwide Express XPR
18
- Worldwide Express XDM
19
- Worldwide Expedited XPD
20
- =end
21
-
22
5
  module Shipping
23
- class UPS < Base
24
-
25
- API_VERSION = "1.0001"
26
-
27
- # For current implementation docs, see http://www.ec.ups.com/ecommerce/techdocs/pdf/RatesandServiceHTML.pdf
28
- # For upcoming XML implementation docs, see http://www.ups.com/gec/techdocs/pdf/dtk_RateXML_V1.zip
29
- def price
30
- @required = [:zip, :country, :sender_zip, :weight]
31
-
32
- @insured_value ||= 0
33
- @country ||= 'US'
34
- @sender_country ||= 'US'
35
- @transaction_type ||= 'GND' # default to UPS ground
36
-
37
- @data = "AppVersion=1.2&AcceptUPSLicenseAgreement=yes&ResponseType=application/x-ups-rss&ActionCode=3&RateChart=Customer+Counter&DCISInd=0&SNDestinationInd1=0&SNDestinationInd2=0&ResidentialInd=$r&PackagingType=00&ServiceLevelCode=#{@transaction_type}&ShipperPostalCode=#{@sender_zip}&ShipperCountry=#{@sender_country}&ConsigneePostalCode=#{@zip}&ConsigneeCountry=#{@country}&PackageActualWeight=#{@weight}&DeclaredValueInsurance=#{@insured_value}"
38
-
39
- get_response "http://www.ups.com/using/services/rave/qcost_dss.cgi"
40
-
41
- price = @response.split("%")
42
- price = price[price.size-2]
43
-
44
- return price.to_f
45
- end
46
-
47
- # See http://www.ups.com/gec/techdocs/pdf/dtk_AddrValidateXML_V1.zip for API info
48
-
49
- def valid_address?( delta = 1.0 )
50
- @required = [:ups_account, :ups_user, :ups_password]
51
-
52
- state = STATES.has_value?(@state.downcase) ? STATES.index(@state.downcase) : @state
53
-
54
- @data = String.new
55
- b = Builder::XmlMarkup.new :target => @data
56
-
57
- b.instruct!
58
- b.AccessRequest {|b|
59
- b.AccessLicenseNumber @ups_account
60
- b.UserId @ups_user
61
- b.Password @ups_password
62
- }
63
- b.instruct!
64
- b.AddressValidationRequest {|b|
65
- b.Request {|b|
66
- b.RequestAction "AV"
67
- b.TransactionReference {|b|
68
- b.CustomerContext "#{@city}, #{state} #{@zip}"
69
- b.XpciVersion API_VERSION
70
- }
71
- }
72
- b.Address {|b|
73
- b.City @city
74
- b.StateProvinceCode state
75
- b.PostalCode @zip
76
- }
77
- }
78
-
79
- get_response "https://wwwcie.ups.com/ups.app/xml/AV"
80
-
81
- if REXML::XPath.first(@response, "//AddressValidationResponse/Response/ResponseStatusCode").text == "1" && REXML::XPath.first(@response, "//AddressValidationResponse/AddressValidationResult/Quality").text.to_f >= delta
82
- return true
83
- else
84
- return false
85
- end
86
- end
87
- end
88
- end
6
+
7
+ class UPS < Base
8
+
9
+ API_VERSION = "1.0001"
10
+
11
+ # For current implementation (XML) docs, see http://www.ups.com/gec/techdocs/pdf/dtk_RateXML_V1.zip
12
+ def price
13
+ @required = [:zip, :country, :sender_zip, :weight]
14
+ @required += [:ups_license_number, :ups_user, :ups_password]
15
+
16
+ @insured_value ||= 0
17
+ @country ||= 'US'
18
+ @sender_country ||= 'US'
19
+ @service_type ||= 'ground' # default to UPS ground
20
+ @ups_url ||= "http://wwwcie.ups.com/ups.app/xml"
21
+ @ups_tool = '/Rate'
22
+
23
+ state = STATES.has_value?(@state.downcase) ? STATES.index(@state.downcase).upcase : @state.upcase unless @state.blank?
24
+ sender_state = STATES.has_value?(@sender_state.downcase) ? STATES.index(@sender_state.downcase).upcase : @sender_state.upcase unless @sender_state.blank?
25
+
26
+ # With UPS need to send two xmls
27
+ # First one to authenticate, second for the request
28
+ b = request_access
29
+ b.instruct!
30
+
31
+ b.RatingServiceSelectionRequest { |b|
32
+ b.Request { |b|
33
+ b.TransactionReference { |b|
34
+ b.CustomerContext 'Rating and Service'
35
+ b.XpciVersion API_VERSION
36
+ }
37
+ b.RequestAction 'Rate'
38
+ }
39
+ b.CustomerClassification { |b|
40
+ b.Code CustomerTypes[@customer_type] || '01'
41
+ }
42
+ b.PickupType { |b|
43
+ b.Code @pickup_type || '01'
44
+ }
45
+ b.Shipment { |b|
46
+ b.Shipper { |b|
47
+ b.Address { |b|
48
+ b.PostalCode @sender_zip
49
+ b.CountryCode @sender_country unless @sender_country.blank?
50
+ b.City @sender_city unless @sender_city.blank?
51
+ b.StateProvinceCode sender_state unless sender_state.blank?
52
+ }
53
+ }
54
+ b.ShipTo { |b|
55
+ b.Address { |b|
56
+ b.PostalCode @zip
57
+ b.CountryCode @country unless @country.blank?
58
+ b.City @city unless @city.blank?
59
+ b.StateProvinceCode state unless state.blank?
60
+ }
61
+ }
62
+ b.Service { |b| # The service code
63
+ b.Code ServiceTypes[@service_type] || '03' # defaults to ground
64
+ }
65
+ b.Package { |b| # Package Details
66
+ b.PackagingType { |b|
67
+ b.Code PackageTypes[@packaging_type] || '02' # defaults to 'your packaging'
68
+ b.Description 'Package'
69
+ }
70
+ b.Description 'Rate Shopping'
71
+ b.PackageWeight { |b|
72
+ b.Weight @weight
73
+ b.UnitOfMeasurement { |b|
74
+ b.Code @weight_units || 'LBS' # or KGS
75
+ }
76
+ }
77
+ b.Dimensions { |b|
78
+ b.UnitOfMeasurement { |b|
79
+ b.Code @measure_units || 'IN'
80
+ }
81
+ b.Length @measure_length || 0
82
+ b.Width @measure_width || 0
83
+ b.Height @measure_height || 0
84
+ }
85
+ b.PackageServiceOptions { |b|
86
+ b.InsuredValue { |b|
87
+ b.CurrencyCode @currency_code || 'US'
88
+ b.MonetaryValue @insured_value
89
+ }
90
+ }
91
+ }
92
+ }
93
+ }
94
+
95
+ get_response @ups_url + @ups_tool
96
+
97
+ return REXML::XPath.first(@response, "//RatingServiceSelectionResponse/RatedShipment/TransportationCharges/MonetaryValue").text.to_f
98
+ rescue
99
+ raise ShippingError, get_error
100
+ end
101
+
102
+ # See http://www.ups.com/gec/techdocs/pdf/dtk_AddrValidateXML_V1.zip for API info
103
+ def valid_address?( delta = 1.0 )
104
+ @required = [:ups_license_number, :ups_user, :ups_password]
105
+ @ups_url ||= "http://wwwcie.ups.com/ups.app/xml"
106
+ @ups_tool = '/AV'
107
+
108
+ state = nil
109
+ if @state:
110
+ state = STATES.has_value?(@state.downcase) ? STATES.index(@state.downcase) : @state
111
+ end
112
+
113
+ b = request_access
114
+ b.instruct!
115
+
116
+ b.AddressValidationRequest {|b|
117
+ b.Request {|b|
118
+ b.RequestAction "AV"
119
+ b.TransactionReference {|b|
120
+ b.CustomerContext "#{@city}, #{state} #{@zip}"
121
+ b.XpciVersion API_VERSION
122
+ }
123
+ }
124
+ b.Address {|b|
125
+ b.City @city
126
+ b.StateProvinceCode state
127
+ b.PostalCode @zip
128
+ }
129
+ }
130
+
131
+ get_response @ups_url + @ups_tool
132
+
133
+ if REXML::XPath.first(@response, "//AddressValidationResponse/Response/ResponseStatusCode").text == "1" && REXML::XPath.first(@response, "//AddressValidationResponse/AddressValidationResult/Quality").text.to_f >= delta
134
+ return true
135
+ else
136
+ return false
137
+ end
138
+ rescue ShippingError
139
+ raise ShippingError, get_error
140
+ end
141
+
142
+ # See Ship-WW-XML.pdf for API info
143
+ # @image_type = [GIF|EPL]
144
+ def label
145
+ @required = [:ups_license_number, :ups_shipper_number, :ups_user, :ups_password]
146
+ @required += [:phone, :email, :company, :address, :city, :state, :zip]
147
+ @required += [:sender_phone, :sender_email, :sender_company, :sender_address, :sender_city, :sender_state, :sender_zip ]
148
+ @ups_url ||= "http://wwwcie.ups.com/ups.app/xml"
149
+ @ups_tool = '/ShipConfirm'
150
+
151
+ state = STATES.has_value?(@state.downcase) ? STATES.index(@state.downcase).upcase : @state.upcase unless @state.blank?
152
+ sender_state = STATES.has_value?(@sender_state.downcase) ? STATES.index(@sender_state.downcase).upcase : @sender_state.upcase unless @sender_state.blank?
153
+
154
+ # make ConfirmRequest and get Confirm Response
155
+ b = request_access
156
+ b.instruct!
157
+
158
+ b.ShipmentConfirmRequest { |b|
159
+ b.Request { |b|
160
+ b.RequestAction "ShipConfirm"
161
+ b.RequestOption "nonvalidate"
162
+ b.TransactionReference { |b|
163
+ b.CustomerContext "#{@city}, #{state} #{@zip}"
164
+ b.XpciVersion API_VERSION
165
+ }
166
+ }
167
+ b.Shipment { |b|
168
+ b.Shipper { |b|
169
+ b.ShipperNumber @ups_shipper_number
170
+ b.Name @sender_name
171
+ b.Address { |b|
172
+ b.AddressLine1 @sender_address unless @sender_address.blank?
173
+ b.PostalCode @sender_zip
174
+ b.CountryCode @sender_country unless @sender_country.blank?
175
+ b.City @sender_city unless @sender_city.blank?
176
+ b.StateProvinceCode sender_state unless sender_state.blank?
177
+ }
178
+ }
179
+ b.ShipFrom { |b|
180
+ b.CompanyName @sender_company
181
+ b.Address { |b|
182
+ b.AddressLine1 @sender_address unless @sender_address.blank?
183
+ b.PostalCode @sender_zip
184
+ b.CountryCode @sender_country unless @sender_country.blank?
185
+ b.City @sender_city unless @sender_city.blank?
186
+ b.StateProvinceCode sender_state unless sender_state.blank?
187
+ }
188
+ }
189
+ b.ShipTo { |b|
190
+ b.CompanyName @company
191
+ b.Address { |b|
192
+ b.AddressLine1 @address unless @address.blank?
193
+ b.PostalCode @zip
194
+ b.CountryCode @country unless @country.blank?
195
+ b.City @city unless @city.blank?
196
+ b.StateProvinceCode state unless state.blank?
197
+ }
198
+ }
199
+ b.PaymentInformation { |b|
200
+ pay_type = PaymentTypes[@pay_type] || 'Prepaid'
201
+
202
+ if pay_type == 'Prepaid'
203
+ b.Prepaid { |b|
204
+ b.BillShipper { |b|
205
+ b.AccountNumber @ups_shipper_number
206
+ }
207
+ }
208
+ elsif pay_type == 'BillThirdParty'
209
+ b.BillThirdParty { |b|
210
+ b.BillThirdPartyShipper { |b|
211
+ b.AccountNumber @billing_account
212
+ b.ThirdParty { |b|
213
+ b.Address { |b|
214
+ b.PostalCode @billing_zip
215
+ b.CountryCode @billing_country
216
+ }
217
+ }
218
+ }
219
+ }
220
+ elsif pay_type == 'FreightCollect'
221
+ b.FreightCollect { |b|
222
+ b.BillReceiver { |b|
223
+ b.AccountNumber @billing_account
224
+ }
225
+ }
226
+ else
227
+ raise ShippingError, "Valid pay_types are 'prepaid', 'bill_third_party', or 'freight_collect'."
228
+ end
229
+ }
230
+ b.Service { |b| # The service code
231
+ b.Code ServiceTypes[@service_type] || '03' # defaults to ground
232
+ }
233
+ b.Package { |b| # Package Details
234
+ b.PackagingType { |b|
235
+ b.Code PackageTypes[@packaging_type] || '02' # defaults to 'your packaging'
236
+ b.Description 'Package'
237
+ }
238
+ b.PackageWeight { |b|
239
+ b.Weight @weight
240
+ b.UnitOfMeasurement { |b|
241
+ b.Code @weight_units || 'LBS' # or KGS
242
+ }
243
+ }
244
+ b.Dimensions { |b|
245
+ b.UnitOfMeasurement { |b|
246
+ b.Code @measure_units || 'IN'
247
+ }
248
+ b.Length @measure_length || 0
249
+ b.Width @measure_width || 0
250
+ b.Height @measure_height || 0
251
+ } if @measure_length || @measure_width || @measure_height
252
+ b.PackageServiceOptions { |b|
253
+ b.InsuredValue { |b|
254
+ b.CurrencyCode @currency_code || 'US'
255
+ b.MonetaryValue @insured_value
256
+ }
257
+ } if @insured_value
258
+ }
259
+ }
260
+ b.LabelSpecification { |b|
261
+ image_type = @image_type || 'GIF' # default to GIF
262
+
263
+ b.LabelPrintMethod { |b|
264
+ b.Code image_type
265
+ }
266
+ if image_type == 'GIF'
267
+ b.HTTPUserAgent 'Mozilla/5.0'
268
+ b.LabelImageFormat { |b|
269
+ b.Code 'GIF'
270
+ }
271
+ elsif image_type == 'EPL'
272
+ b.LabelStockSize { |b|
273
+ b.Height '4'
274
+ b.Width '6'
275
+ }
276
+ else
277
+ raise ShippingError, "Valid image_types are 'EPL' or 'GIF'."
278
+ end
279
+ }
280
+ }
281
+
282
+ # get ConfirmResponse
283
+ get_response @ups_url + @ups_tool
284
+ begin
285
+ shipment_digest = REXML::XPath.first(@response, '//ShipmentConfirmResponse/ShipmentDigest').text
286
+ rescue
287
+ raise ShippingError, get_error
288
+ end
289
+
290
+ # make AcceptRequest and get AcceptResponse
291
+ @ups_tool = '/ShipAccept'
292
+
293
+ b = request_access
294
+ b.instruct!
295
+
296
+ b.ShipmentAcceptRequest { |b|
297
+ b.Request { |b|
298
+ b.RequestAction "ShipAccept"
299
+ b.TransactionReference { |b|
300
+ b.CustomerContext "#{@city}, #{state} #{@zip}"
301
+ b.XpciVersion API_VERSION
302
+ }
303
+ }
304
+ b.ShipmentDigest shipment_digest
305
+ }
306
+
307
+ # get AcceptResponse
308
+ get_response @ups_url + @ups_tool
309
+
310
+ begin
311
+ response = Hash.new
312
+ response[:tracking_number] = REXML::XPath.first(@response, "//ShipmentAcceptResponse/ShipmentResults/PackageResults/TrackingNumber").text
313
+ response[:encoded_image] = REXML::XPath.first(@response, "//ShipmentAcceptResponse/ShipmentResults/PackageResults/LabelImage/GraphicImage").text
314
+ response[:image] = Tempfile.new("shipping_label")
315
+ response[:image].write Base64.decode64( response[:encoded_image] )
316
+ response[:image].rewind
317
+ rescue
318
+ raise ShippingError, get_error
319
+ end
320
+
321
+ # allows for things like fedex.label.url
322
+ def response.method_missing(name, *args)
323
+ has_key?(name) ? self[name] : super
324
+ end
325
+
326
+ # don't allow people to edit the response
327
+ response.freeze
328
+ end
329
+
330
+ def void(tracking_number)
331
+ @required = [:ups_license_number, :ups_shipper_number, :ups_user, :ups_password]
332
+ @ups_url ||= "http://wwwcie.ups.com/ups.app/xml"
333
+ @ups_tool = '/Void'
334
+
335
+ # make ConfirmRequest and get Confirm Response
336
+ b = request_access
337
+ b.instruct!
338
+
339
+ b.VoidShipmentRequest { |b|
340
+ b.Request { |b|
341
+ b.RequestAction "Void"
342
+ b.TransactionReference { |b|
343
+ b.CustomerContext "Void #{@tracking_number}"
344
+ b.XpciVersion API_VERSION
345
+ }
346
+ }
347
+ b.ShipmentIdentificationNumber tracking_number
348
+ }
349
+
350
+ # get VoidResponse
351
+ get_response @ups_url + @ups_tool
352
+ status = REXML::XPath.first(@response, '//VoidShipmentResponse/Response/ResponseStatusCode').text
353
+ raise ShippingError, get_error if status == '0'
354
+ return true if status == '1'
355
+ end
356
+
357
+ private
358
+
359
+ def request_access
360
+ @data = String.new
361
+ b = Builder::XmlMarkup.new :target => @data
362
+
363
+ b.instruct!
364
+ b.AccessRequest {|b|
365
+ b.AccessLicenseNumber @ups_license_number
366
+ b.UserId @ups_user
367
+ b.Password @ups_password
368
+ }
369
+ return b
370
+ end
371
+
372
+ def get_error
373
+ return if @response.class != REXML::Document
374
+
375
+ error = REXML::XPath.first(@response, '//*/Response/Error')
376
+ return if !error
377
+
378
+ severity = REXML::XPath.first(error, '//ErrorSeverity').text
379
+ code = REXML::XPath.first(error, '//ErrorCode').text
380
+ description = REXML::XPath.first(error, '//ErrorDescription').text
381
+ begin
382
+ location = REXML::XPath.first(error, '//ErrorLocation/ErrorLocationElementName').text
383
+ rescue
384
+ location = 'unknown'
385
+ end
386
+ return "#{severity} Error ##{code} @ #{location}: #{description}"
387
+ end
388
+
389
+ # The following type hashes are to allow cross-api data retrieval
390
+ PackageTypes = {
391
+ "ups_envelope" => "01",
392
+ "your_packaging" => "02",
393
+ "ups_tube" => "03",
394
+ "ups_pak" => "04",
395
+ "ups_box" => "21",
396
+ "fedex_25_kg_box" => "24",
397
+ "fedex_10_kg_box" => "25"
398
+ }
399
+
400
+ ServiceTypes = {
401
+ "next_day" => "01",
402
+ "2day" => "02",
403
+ "ground_service" => "03",
404
+ "worldwide_express" => "07",
405
+ "worldwide_expedited" => "08",
406
+ "standard" => "11",
407
+ "3day" => "12",
408
+ "next_day_saver" => "13",
409
+ "next_day_early" => "14",
410
+ "worldwide_express_plus" => "54",
411
+ "2day_early" => "59"
412
+ }
413
+
414
+ PickupTypes = {
415
+ 'daily_pickup' => '01',
416
+ 'customer_counter' => '03',
417
+ 'one_time_pickup' => '06',
418
+ 'on_call' => '07',
419
+ 'suggested_retail_rates' => '11',
420
+ 'letter_center' => '19',
421
+ 'air_service_center' => '20'
422
+ }
423
+
424
+ CustomerTypes = {
425
+ 'wholesale' => '01',
426
+ 'ocassional' => '02',
427
+ 'retail' => '04'
428
+ }
429
+
430
+ PaymentTypes = {
431
+ 'prepaid' => 'Prepaid',
432
+ 'consignee' => 'Consignee', # TODO: Implement
433
+ 'bill_third_party' => 'BillThirdParty',
434
+ 'freight_collect' => 'FreightCollect'
435
+ }
436
+ end
437
+ end