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/README +10 -6
- data/lib/shipping.rb +2 -0
- data/lib/shipping/base.rb +159 -156
- data/lib/shipping/fedex.rb +463 -438
- data/lib/shipping/ups.rb +432 -83
- data/test/base/base_test.rb +10 -4
- data/test/fedex/fedex_test.rb +55 -22
- data/test/ups/ups_test.rb +62 -6
- metadata +42 -36
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
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
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
|