active_shipping 1.0.0.pre4 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data.tar.gz.sig +0 -0
- data/.yardopts +0 -1
- data/CHANGELOG.md +17 -0
- data/CONTRIBUTING.md +2 -2
- data/Gemfile.activesupport32 +1 -0
- data/README.md +1 -1
- data/Rakefile +1 -1
- data/active_shipping.gemspec +2 -2
- data/lib/active_shipping.rb +2 -3
- data/lib/active_shipping/carriers.rb +1 -0
- data/lib/active_shipping/carriers/canada_post_pws.rb +281 -266
- data/lib/active_shipping/carriers/correios.rb +285 -0
- data/lib/active_shipping/carriers/fedex.rb +205 -199
- data/lib/active_shipping/carriers/stamps.rb +218 -219
- data/lib/active_shipping/carriers/ups.rb +287 -48
- data/lib/active_shipping/carriers/usps.rb +3 -3
- data/lib/active_shipping/delivery_date_estimate.rb +21 -0
- data/lib/active_shipping/delivery_date_estimates_response.rb +11 -0
- data/lib/active_shipping/errors.rb +20 -1
- data/lib/active_shipping/location.rb +0 -5
- data/lib/active_shipping/response.rb +0 -15
- data/lib/active_shipping/version.rb +1 -1
- data/test/credentials.yml +11 -3
- data/test/fixtures/xml/correios/book_response.xml +13 -0
- data/test/fixtures/xml/correios/book_response_invalid.xml +13 -0
- data/test/fixtures/xml/correios/clothes_response.xml +43 -0
- data/test/fixtures/xml/correios/poster_response.xml +23 -0
- data/test/fixtures/xml/correios/shoes_response.xml +43 -0
- data/test/fixtures/xml/fedex/tracking_request.xml +13 -11
- data/test/fixtures/xml/fedex/tracking_response_bad_tracking_number.xml +20 -0
- data/test/fixtures/xml/fedex/tracking_response_delivered_at_door.xml +254 -0
- data/test/fixtures/xml/fedex/tracking_response_delivered_at_facility.xml +403 -0
- data/test/fixtures/xml/fedex/tracking_response_delivered_with_signature.xml +269 -0
- data/test/fixtures/xml/fedex/tracking_response_in_transit.xml +127 -0
- data/test/fixtures/xml/fedex/tracking_response_multiple_results.xml +100 -0
- data/test/fixtures/xml/fedex/tracking_response_not_found.xml +52 -0
- data/test/fixtures/xml/fedex/tracking_response_shipment_exception.xml +209 -0
- data/test/fixtures/xml/ups/delivery_dates_response.xml +140 -0
- data/test/fixtures/xml/ups/package_exceeds_maximum_length.xml +12 -0
- data/test/fixtures/xml/ups/rate_single_service.xml +54 -0
- data/test/fixtures/xml/ups/rescheduled_shipment.xml +204 -0
- data/test/fixtures/xml/ups/test_real_home_as_residential_destination_response.xml +290 -1
- data/test/fixtures/xml/usps/delivered_extended_tracking_response.xml +11 -0
- data/test/remote/canada_post_pws_platform_test.rb +35 -22
- data/test/remote/canada_post_pws_test.rb +32 -40
- data/test/remote/correios_test.rb +83 -0
- data/test/remote/fedex_test.rb +95 -13
- data/test/remote/stamps_test.rb +1 -0
- data/test/remote/ups_test.rb +77 -40
- data/test/remote/usps_test.rb +13 -1
- data/test/test_helper.rb +12 -2
- data/test/unit/carriers/canada_post_pws_rating_test.rb +66 -59
- data/test/unit/carriers/canada_post_pws_shipping_test.rb +34 -23
- data/test/unit/carriers/correios_test.rb +244 -0
- data/test/unit/carriers/fedex_test.rb +161 -156
- data/test/unit/carriers/ups_test.rb +193 -1
- data/test/unit/carriers/usps_test.rb +14 -0
- data/test/unit/location_test.rb +0 -10
- metadata +63 -46
- metadata.gz.sig +0 -0
- data/lib/vendor/test_helper.rb +0 -6
- data/lib/vendor/xml_node/README +0 -36
- data/lib/vendor/xml_node/Rakefile +0 -21
- data/lib/vendor/xml_node/benchmark/bench_generation.rb +0 -30
- data/lib/vendor/xml_node/init.rb +0 -1
- data/lib/vendor/xml_node/lib/xml_node.rb +0 -221
- data/lib/vendor/xml_node/test/test_generating.rb +0 -89
- data/lib/vendor/xml_node/test/test_parsing.rb +0 -40
- data/test/fixtures/xml/fedex/tracking_response.xml +0 -151
- data/test/fixtures/xml/fedex/tracking_response_empty_destination.xml +0 -76
- data/test/fixtures/xml/fedex/tracking_response_no_destination.xml +0 -139
- data/test/fixtures/xml/fedex/tracking_response_no_ship_time.xml +0 -150
- data/test/fixtures/xml/fedex/tracking_response_with_estimated_delivery_date.xml +0 -95
- data/test/fixtures/xml/fedex/tracking_response_with_shipper_address.xml +0 -71
@@ -0,0 +1,285 @@
|
|
1
|
+
# -*- encoding utf-8 -*-
|
2
|
+
|
3
|
+
module ActiveShipping
|
4
|
+
class Correios < Carrier
|
5
|
+
|
6
|
+
cattr_reader :name
|
7
|
+
@@name = "Correios do Brasil"
|
8
|
+
|
9
|
+
def find_rates(origin, destination, packages, options = {})
|
10
|
+
options = @options.merge(options)
|
11
|
+
|
12
|
+
request = CorreiosRequest.new(origin, destination, packages, options)
|
13
|
+
response = request.create_response(perform(request.urls))
|
14
|
+
|
15
|
+
response
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.available_services
|
19
|
+
AVAILABLE_SERVICES
|
20
|
+
end
|
21
|
+
|
22
|
+
protected
|
23
|
+
|
24
|
+
DEFAULT_SERVICES = [41106, 40010]
|
25
|
+
AVAILABLE_SERVICES = {
|
26
|
+
41106 => 'PAC sem contrato',
|
27
|
+
41068 => 'PAC com contrato',
|
28
|
+
41300 => 'PAC para grandes formatos',
|
29
|
+
40010 => 'SEDEX sem contrato',
|
30
|
+
40045 => 'SEDEX a Cobrar, sem contrato',
|
31
|
+
40126 => 'SEDEX a Cobrar, com contrato',
|
32
|
+
40215 => 'SEDEX 10, sem contrato',
|
33
|
+
40290 => 'SEDEX Hoje, sem contrato',
|
34
|
+
40096 => 'SEDEX com contrato',
|
35
|
+
40436 => 'SEDEX com contrato',
|
36
|
+
40444 => 'SEDEX com contrato',
|
37
|
+
40568 => 'SEDEX com contrato',
|
38
|
+
40606 => 'SEDEX com contrato',
|
39
|
+
81019 => 'e-SEDEX, com contrato',
|
40
|
+
81027 => 'e-SEDEX Prioritário, com contrato',
|
41
|
+
81035 => 'e-SEDEX Express, com contrato',
|
42
|
+
81868 => '(Grupo 1) e-SEDEX, com contrato',
|
43
|
+
81833 => '(Grupo 2) e-SEDEX, com contrato',
|
44
|
+
81850 => '(Grupo 3) e-SEDEX, com contrato'
|
45
|
+
}.freeze
|
46
|
+
|
47
|
+
def perform(urls)
|
48
|
+
urls.map { |url| ssl_get(url) }
|
49
|
+
end
|
50
|
+
|
51
|
+
class CorreiosRequest
|
52
|
+
|
53
|
+
URL = "http://ws.correios.com.br/calculador/CalcPrecoPrazo.aspx"
|
54
|
+
|
55
|
+
RETURN_TYPE = 'xml'
|
56
|
+
RETURN_INFORMATION_TYPE = {
|
57
|
+
:prices => '1',
|
58
|
+
:time => '2',
|
59
|
+
:prices_and_time => '3'
|
60
|
+
}
|
61
|
+
|
62
|
+
attr_reader :origin, :destination, :urls
|
63
|
+
|
64
|
+
def initialize(origin, destination, packages, options)
|
65
|
+
@options = options
|
66
|
+
@origin = origin
|
67
|
+
@destination = destination
|
68
|
+
|
69
|
+
packages = packages.map do |package|
|
70
|
+
CorreiosPackage.new(package, 1)
|
71
|
+
end
|
72
|
+
|
73
|
+
@params = {
|
74
|
+
company_id: options[:company_id],
|
75
|
+
password: options[:password],
|
76
|
+
service_type: service_type,
|
77
|
+
origin_zip: origin.zip,
|
78
|
+
destination_zip: destination.zip,
|
79
|
+
mao_propria_extra: parse_boolean(options[:mao_propria_extra]),
|
80
|
+
declared_value_extra: parse_currency(options[:declared_value_extra]),
|
81
|
+
delivery_notice_extra: parse_boolean(options[:delivery_notice_extra]),
|
82
|
+
return_type: RETURN_TYPE,
|
83
|
+
return_information: RETURN_INFORMATION_TYPE[:prices]
|
84
|
+
}
|
85
|
+
@urls = packages.map { |package| create_url(package).to_s }
|
86
|
+
end
|
87
|
+
|
88
|
+
def create_response(raw_xmls)
|
89
|
+
correios_response = CorreiosResponse.new(self, raw_xmls)
|
90
|
+
correios_response.rate_response
|
91
|
+
end
|
92
|
+
|
93
|
+
private
|
94
|
+
|
95
|
+
def parse_boolean(param)
|
96
|
+
param ? 'S' : 'N'
|
97
|
+
end
|
98
|
+
|
99
|
+
def parse_currency(param)
|
100
|
+
param.nil? ? '0' : param.to_s.gsub('.', ',')
|
101
|
+
end
|
102
|
+
|
103
|
+
def build_params(package)
|
104
|
+
@params.merge(package.params)
|
105
|
+
end
|
106
|
+
|
107
|
+
def hash_query_string(params)
|
108
|
+
{
|
109
|
+
'nCdEmpresa' => params[:company_id],
|
110
|
+
'sDsSenha' => params[:password],
|
111
|
+
'nCdServico' => params[:service_type],
|
112
|
+
'sCepOrigem' => params[:origin_zip],
|
113
|
+
'sCepDestino' => params[:destination_zip],
|
114
|
+
'nVlPeso' => params[:weight],
|
115
|
+
'nCdFormato' => params[:format],
|
116
|
+
'nVlComprimento' => params[:length],
|
117
|
+
'nVlAltura' => params[:height],
|
118
|
+
'nVlLargura' => params[:width],
|
119
|
+
'nVlDiametro' => params[:diameter],
|
120
|
+
'sCdMaoPropria' => params[:mao_propria_extra],
|
121
|
+
'nVlValorDeclarado' => params[:declared_value_extra],
|
122
|
+
'sCdAvisoRecebimento' => params[:delivery_notice_extra],
|
123
|
+
'nIndicaCalculo' => params[:return_information],
|
124
|
+
'StrRetorno' => params[:return_type]
|
125
|
+
}
|
126
|
+
end
|
127
|
+
|
128
|
+
def query_string(package)
|
129
|
+
params = build_params(package)
|
130
|
+
hash_query_string(params)
|
131
|
+
end
|
132
|
+
|
133
|
+
def create_url(package)
|
134
|
+
scheme, userinfo, host, port, registry, path, opaque, query, fragment = URI::split(URL)
|
135
|
+
|
136
|
+
query ||= ""
|
137
|
+
query = CGI.parse(query)
|
138
|
+
.merge(query_string(package))
|
139
|
+
.to_query
|
140
|
+
|
141
|
+
URI::HTTP.new(scheme, userinfo, host, port, registry, path, opaque, query, fragment)
|
142
|
+
end
|
143
|
+
|
144
|
+
def service_type
|
145
|
+
@options[:services].nil? ? DEFAULT_SERVICES.join(',') : @options[:services].join(',')
|
146
|
+
end
|
147
|
+
|
148
|
+
end
|
149
|
+
|
150
|
+
class CorreiosRateResponse < ActiveShipping::RateResponse
|
151
|
+
attr_reader :raw_responses, :urls
|
152
|
+
|
153
|
+
def initialize(success, message, params = {}, options = {})
|
154
|
+
@raw_responses = options[:raw_responses]
|
155
|
+
@urls = options[:urls]
|
156
|
+
super
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
class CorreiosResponse
|
161
|
+
|
162
|
+
def initialize(request, raw_xmls)
|
163
|
+
@request = request
|
164
|
+
@raw_xmls = raw_xmls
|
165
|
+
@documents = raw_xmls.map { |xml| Nokogiri::XML(xml) }
|
166
|
+
end
|
167
|
+
|
168
|
+
def rate_response
|
169
|
+
@rates = rates
|
170
|
+
CorreiosRateResponse.new(true, nil, params_options, response_options)
|
171
|
+
rescue => error
|
172
|
+
CorreiosRateResponse.new(false, error.message, {}, response_options)
|
173
|
+
end
|
174
|
+
|
175
|
+
private
|
176
|
+
|
177
|
+
def response_options
|
178
|
+
{
|
179
|
+
:rates => @rates,
|
180
|
+
:raw_responses => @raw_xmls,
|
181
|
+
:urls => @request.urls
|
182
|
+
}
|
183
|
+
end
|
184
|
+
|
185
|
+
def params_options
|
186
|
+
{ :responses => @documents }
|
187
|
+
end
|
188
|
+
|
189
|
+
def normalized_services
|
190
|
+
services = @documents.map { |document| document.root.elements }.flatten
|
191
|
+
services = services.group_by do |service_xml|
|
192
|
+
raise(error_message(service_xml)) if error?(service_xml)
|
193
|
+
service_code(service_xml)
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
def rates_array
|
198
|
+
services = normalized_services.map do |service_id, elements|
|
199
|
+
total_price = elements.sum { |element| price(element) }
|
200
|
+
{ :service_code => service_id, :total_price => total_price, :currency => "BRL" }
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
def rates
|
205
|
+
rates_array.map { |rate_hash| create_rate_estimate(rate_hash) }
|
206
|
+
end
|
207
|
+
|
208
|
+
def create_rate_estimate(rate_hash)
|
209
|
+
RateEstimate.new(@request.origin, @request.destination, Correios.name, AVAILABLE_SERVICES[rate_hash[:service_code]], rate_hash)
|
210
|
+
end
|
211
|
+
|
212
|
+
def error?(xml_item)
|
213
|
+
error_id = error_id(xml_item)
|
214
|
+
!error_id.empty? && error_id != "0"
|
215
|
+
end
|
216
|
+
|
217
|
+
def error_id(xml_item)
|
218
|
+
xml_item.css('Erro').text
|
219
|
+
end
|
220
|
+
|
221
|
+
def error_message(xml_item)
|
222
|
+
xml_item.css('MsgErro').text
|
223
|
+
end
|
224
|
+
|
225
|
+
def service_code(xml_item)
|
226
|
+
xml_item.css('Codigo').text.to_i
|
227
|
+
end
|
228
|
+
|
229
|
+
def price(xml_item)
|
230
|
+
xml_item.css('Valor').text.gsub(',', '.').to_f
|
231
|
+
end
|
232
|
+
|
233
|
+
end
|
234
|
+
|
235
|
+
class CorreiosPackage
|
236
|
+
attr_reader :params
|
237
|
+
|
238
|
+
FORMAT = {
|
239
|
+
:package => 1,
|
240
|
+
:roll => 2,
|
241
|
+
:envelope => 3
|
242
|
+
}
|
243
|
+
|
244
|
+
def initialize(package, format)
|
245
|
+
@package = package
|
246
|
+
|
247
|
+
@params = {
|
248
|
+
:format => format,
|
249
|
+
:weight => weight,
|
250
|
+
:width => width,
|
251
|
+
:length => length,
|
252
|
+
:height => height(format),
|
253
|
+
:diameter => diameter
|
254
|
+
}
|
255
|
+
end
|
256
|
+
|
257
|
+
private
|
258
|
+
|
259
|
+
def weight
|
260
|
+
@package.kg
|
261
|
+
end
|
262
|
+
|
263
|
+
def width
|
264
|
+
@package.cm(:width)
|
265
|
+
end
|
266
|
+
|
267
|
+
def length
|
268
|
+
@package.cm(:length)
|
269
|
+
end
|
270
|
+
|
271
|
+
def height(format)
|
272
|
+
return 0 if format == FORMAT[:envelope]
|
273
|
+
return diameter if @package.cylinder?
|
274
|
+
@package.cm(:height)
|
275
|
+
end
|
276
|
+
|
277
|
+
def diameter
|
278
|
+
return 0 unless @package.cylinder?
|
279
|
+
@package.cm(:width)
|
280
|
+
end
|
281
|
+
|
282
|
+
end
|
283
|
+
|
284
|
+
end
|
285
|
+
end
|
@@ -1,12 +1,9 @@
|
|
1
|
-
# FedEx module by Jimmy Baker
|
2
|
-
# http://github.com/jimmyebaker
|
3
|
-
|
4
|
-
require 'date'
|
5
1
|
module ActiveShipping
|
6
|
-
|
7
|
-
#
|
8
|
-
#
|
9
|
-
#
|
2
|
+
|
3
|
+
# FedEx carrier implementation.
|
4
|
+
#
|
5
|
+
# FedEx module by Jimmy Baker (http://github.com/jimmyebaker)
|
6
|
+
# Documentation can be found here: http://images.fedex.com/us/developer/product/WebServices/MyWebHelp/PropDevGuide.pdf
|
10
7
|
class FedEx < Carrier
|
11
8
|
self.retry_safe = true
|
12
9
|
|
@@ -87,7 +84,8 @@ module ActiveShipping
|
|
87
84
|
'ground_customer_reference' => 'GROUND_CUSTOMER_REFERENCE',
|
88
85
|
'ground_po' => 'GROUND_PO',
|
89
86
|
'express_reference' => 'EXPRESS_REFERENCE',
|
90
|
-
'express_mps_master' => 'EXPRESS_MPS_MASTER'
|
87
|
+
'express_mps_master' => 'EXPRESS_MPS_MASTER',
|
88
|
+
'shipper_reference' => 'SHIPPER_REFERENCE',
|
91
89
|
}
|
92
90
|
|
93
91
|
TRANSIT_TIMES = %w(UNKNOWN ONE_DAY TWO_DAYS THREE_DAYS FOUR_DAYS FIVE_DAYS SIX_DAYS SEVEN_DAYS EIGHT_DAYS NINE_DAYS TEN_DAYS ELEVEN_DAYS TWELVE_DAYS THIRTEEN_DAYS FOURTEEN_DAYS FIFTEEN_DAYS SIXTEEN_DAYS SEVENTEEN_DAYS EIGHTEEN_DAYS)
|
@@ -143,9 +141,8 @@ module ActiveShipping
|
|
143
141
|
rate_request = build_rate_request(origin, destination, packages, options)
|
144
142
|
|
145
143
|
xml = commit(save_request(rate_request), (options[:test] || false))
|
146
|
-
response = remove_version_prefix(xml)
|
147
144
|
|
148
|
-
parse_rate_response(origin, destination, packages,
|
145
|
+
parse_rate_response(origin, destination, packages, xml, options)
|
149
146
|
end
|
150
147
|
|
151
148
|
def find_tracking_info(tracking_number, options = {})
|
@@ -153,8 +150,7 @@ module ActiveShipping
|
|
153
150
|
|
154
151
|
tracking_request = build_tracking_request(tracking_number, options)
|
155
152
|
xml = commit(save_request(tracking_request), (options[:test] || false))
|
156
|
-
|
157
|
-
parse_tracking_response(response, options)
|
153
|
+
parse_tracking_response(xml, options)
|
158
154
|
end
|
159
155
|
|
160
156
|
protected
|
@@ -162,90 +158,90 @@ module ActiveShipping
|
|
162
158
|
def build_rate_request(origin, destination, packages, options = {})
|
163
159
|
imperial = %w(US LR MM).include?(origin.country_code(:alpha2))
|
164
160
|
|
165
|
-
|
166
|
-
|
167
|
-
|
161
|
+
xml_builder = Nokogiri::XML::Builder.new do |xml|
|
162
|
+
xml.RateRequest(xmlns: 'http://fedex.com/ws/rate/v13') do
|
163
|
+
build_request_header(xml)
|
164
|
+
build_version_node(xml, 'crs', 13, 0 ,0)
|
168
165
|
|
169
|
-
|
170
|
-
|
171
|
-
# Returns saturday delivery shipping options when available
|
172
|
-
root_node << XmlNode.new('VariableOptions', 'SATURDAY_DELIVERY')
|
166
|
+
# Returns delivery dates
|
167
|
+
xml.ReturnTransitAndCommit(true)
|
173
168
|
|
174
|
-
|
175
|
-
|
169
|
+
# Returns saturday delivery shipping options when available
|
170
|
+
xml.VariableOptions('SATURDAY_DELIVERY')
|
176
171
|
|
177
|
-
|
172
|
+
xml.RequestedShipment do
|
173
|
+
xml.ShipTimestamp(ship_timestamp(options[:turn_around_time]).iso8601(0))
|
178
174
|
|
179
|
-
|
180
|
-
# fedex api wants this up here otherwise request returns an error
|
181
|
-
rs << XmlNode.new('DropoffType', options[:dropoff_type] || 'REGULAR_PICKUP')
|
182
|
-
rs << XmlNode.new('PackagingType', options[:packaging_type] || 'YOUR_PACKAGING')
|
183
|
-
end
|
184
|
-
|
185
|
-
rs << build_location_node('Shipper', (options[:shipper] || origin))
|
186
|
-
rs << build_location_node('Recipient', destination)
|
187
|
-
if options[:shipper] and options[:shipper] != origin
|
188
|
-
rs << build_location_node('Origin', origin)
|
189
|
-
end
|
175
|
+
freight = has_freight?(options)
|
190
176
|
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
rs << build_freight_shipment_detail_node(freight_options, packages, imperial)
|
196
|
-
rs << build_rate_request_types_node
|
197
|
-
else
|
198
|
-
# build xml for non-freight rate requests
|
199
|
-
rs << XmlNode.new('SmartPostDetail') do |spd|
|
200
|
-
spd << XmlNode.new('Indicia', options[:smart_post_indicia] || 'PARCEL_SELECT')
|
201
|
-
spd << XmlNode.new('HubId', options[:smart_post_hub_id] || 5902) # default to LA
|
177
|
+
unless freight
|
178
|
+
# fedex api wants this up here otherwise request returns an error
|
179
|
+
xml.DropoffType(options[:dropoff_type] || 'REGULAR_PICKUP')
|
180
|
+
xml.PackagingType(options[:packaging_type] || 'YOUR_PACKAGING')
|
202
181
|
end
|
203
182
|
|
204
|
-
|
205
|
-
|
206
|
-
|
183
|
+
build_location_node(xml, 'Shipper', options[:shipper] || origin)
|
184
|
+
build_location_node(xml, 'Recipient', destination)
|
185
|
+
if options[:shipper] && options[:shipper] != origin
|
186
|
+
build_location_node(xml, 'Origin', origin)
|
187
|
+
end
|
207
188
|
|
189
|
+
if freight
|
190
|
+
freight_options = options[:freight]
|
191
|
+
build_shipping_charges_payment_node(xml, freight_options)
|
192
|
+
build_freight_shipment_detail_node(xml, freight_options, packages, imperial)
|
193
|
+
build_rate_request_types_node(xml)
|
194
|
+
else
|
195
|
+
xml.SmartPostDetail do
|
196
|
+
xml.Indicia(options[:smart_post_indicia] || 'PARCEL_SELECT')
|
197
|
+
xml.HubId(options[:smart_post_hub_id] || 5902) # default to LA
|
198
|
+
end
|
199
|
+
|
200
|
+
build_rate_request_types_node(xml)
|
201
|
+
xml.PackageCount(packages.size)
|
202
|
+
build_packages_nodes(xml, packages, imperial)
|
203
|
+
end
|
208
204
|
end
|
209
205
|
end
|
210
206
|
end
|
211
|
-
|
207
|
+
xml_builder.to_xml
|
212
208
|
end
|
213
209
|
|
214
|
-
def build_packages_nodes(packages, imperial)
|
210
|
+
def build_packages_nodes(xml, packages, imperial)
|
215
211
|
packages.map do |pkg|
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
212
|
+
xml.RequestedPackageLineItems do
|
213
|
+
xml.GroupPackageCount(1)
|
214
|
+
build_package_weight_node(xml, pkg, imperial)
|
215
|
+
build_package_dimensions_node(xml, pkg, imperial)
|
220
216
|
end
|
221
217
|
end
|
222
218
|
end
|
223
219
|
|
224
|
-
def build_shipping_charges_payment_node(freight_options)
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
220
|
+
def build_shipping_charges_payment_node(xml, freight_options)
|
221
|
+
xml.ShippingChargesPayment do
|
222
|
+
xml.PaymentType(freight_options[:payment_type])
|
223
|
+
xml.Payor do
|
224
|
+
xml.ResponsibleParty do
|
229
225
|
# TODO: case of different freight account numbers?
|
230
|
-
|
226
|
+
xml.AccountNumber(freight_options[:account])
|
231
227
|
end
|
232
228
|
end
|
233
229
|
end
|
234
230
|
end
|
235
231
|
|
236
|
-
def build_freight_shipment_detail_node(freight_options, packages, imperial)
|
237
|
-
|
232
|
+
def build_freight_shipment_detail_node(xml, freight_options, packages, imperial)
|
233
|
+
xml.FreightShipmentDetail do
|
238
234
|
# TODO: case of different freight account numbers?
|
239
|
-
|
240
|
-
|
241
|
-
|
235
|
+
xml.FedExFreightAccountNumber(freight_options[:account])
|
236
|
+
build_location_node(xml, 'FedExFreightBillingContactAndAddress', freight_options[:billing_location])
|
237
|
+
xml.Role(freight_options[:role])
|
242
238
|
|
243
239
|
packages.each do |pkg|
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
240
|
+
xml.LineItems do
|
241
|
+
xml.FreightClass(freight_options[:freight_class])
|
242
|
+
xml.Packaging(freight_options[:packaging])
|
243
|
+
build_package_weight_node(xml, pkg, imperial)
|
244
|
+
build_package_dimensions_node(xml, pkg, imperial)
|
249
245
|
end
|
250
246
|
end
|
251
247
|
end
|
@@ -255,125 +251,116 @@ module ActiveShipping
|
|
255
251
|
options[:freight] && options[:freight].present?
|
256
252
|
end
|
257
253
|
|
258
|
-
def build_package_weight_node(pkg, imperial)
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
end
|
263
|
-
end
|
264
|
-
|
265
|
-
def build_version_node
|
266
|
-
XmlNode.new('Version') do |version_node|
|
267
|
-
version_node << XmlNode.new('ServiceId', 'crs')
|
268
|
-
version_node << XmlNode.new('Major', '13')
|
269
|
-
version_node << XmlNode.new('Intermediate', '0')
|
270
|
-
version_node << XmlNode.new('Minor', '0')
|
254
|
+
def build_package_weight_node(xml, pkg, imperial)
|
255
|
+
xml.Weight do
|
256
|
+
xml.Units(imperial ? 'LB' : 'KG')
|
257
|
+
xml.Value([((imperial ? pkg.lbs : pkg.kgs).to_f * 1000).round / 1000.0, 0.1].max)
|
271
258
|
end
|
272
259
|
end
|
273
260
|
|
274
|
-
def build_package_dimensions_node(pkg, imperial)
|
275
|
-
|
261
|
+
def build_package_dimensions_node(xml, pkg, imperial)
|
262
|
+
xml.Dimensions do
|
276
263
|
[:length, :width, :height].each do |axis|
|
277
264
|
value = ((imperial ? pkg.inches(axis) : pkg.cm(axis)).to_f * 1000).round / 1000.0 # 3 decimals
|
278
|
-
|
265
|
+
xml.public_send(axis.to_s.capitalize, value.ceil)
|
279
266
|
end
|
280
|
-
|
267
|
+
xml.Units(imperial ? 'IN' : 'CM')
|
281
268
|
end
|
282
269
|
end
|
283
270
|
|
284
|
-
def build_rate_request_types_node(type = 'ACCOUNT')
|
285
|
-
|
271
|
+
def build_rate_request_types_node(xml, type = 'ACCOUNT')
|
272
|
+
xml.RateRequestTypes(type)
|
286
273
|
end
|
287
274
|
|
288
275
|
def build_tracking_request(tracking_number, options = {})
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
276
|
+
xml_builder = Nokogiri::XML::Builder.new do |xml|
|
277
|
+
xml.TrackRequest(xmlns: 'http://fedex.com/ws/track/v7') do
|
278
|
+
build_request_header(xml)
|
279
|
+
build_version_node(xml, 'trck', 7, 0, 0)
|
280
|
+
|
281
|
+
xml.SelectionDetails do
|
282
|
+
xml.PackageIdentifier do
|
283
|
+
xml.Type(PACKAGE_IDENTIFIER_TYPES[options[:package_identifier_type] || 'tracking_number'])
|
284
|
+
xml.Value(tracking_number)
|
285
|
+
end
|
299
286
|
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
287
|
+
xml.ShipDateRangeBegin(options[:ship_date_range_begin]) if options[:ship_date_range_begin]
|
288
|
+
xml.ShipDateRangeEnd(options[:ship_date_range_end]) if options[:ship_date_range_end]
|
289
|
+
xml.TrackingNumberUniqueIdentifier(options[:unique_identifier]) if options[:unique_identifier]
|
290
|
+
end
|
304
291
|
|
305
|
-
|
306
|
-
|
307
|
-
root_node << XmlNode.new('IncludeDetailedScans', 1)
|
292
|
+
xml.ProcessingOptions('INCLUDE_DETAILED_SCANS')
|
293
|
+
end
|
308
294
|
end
|
309
|
-
|
295
|
+
xml_builder.to_xml
|
310
296
|
end
|
311
297
|
|
312
|
-
def build_request_header
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
298
|
+
def build_request_header(xml)
|
299
|
+
xml.WebAuthenticationDetail do
|
300
|
+
xml.UserCredential do
|
301
|
+
xml.Key(@options[:key])
|
302
|
+
xml.Password(@options[:password])
|
317
303
|
end
|
318
304
|
end
|
319
305
|
|
320
|
-
|
321
|
-
|
322
|
-
|
306
|
+
xml.ClientDetail do
|
307
|
+
xml.AccountNumber(@options[:account])
|
308
|
+
xml.MeterNumber(@options[:login])
|
323
309
|
end
|
324
310
|
|
325
|
-
|
326
|
-
|
311
|
+
xml.TransactionDetail do
|
312
|
+
xml.CustomerTransactionId(@options[:transaction_id] || 'ActiveShipping') # TODO: Need to do something better with this...
|
327
313
|
end
|
328
|
-
|
329
|
-
[web_authentication_detail, client_detail, trasaction_detail]
|
330
314
|
end
|
331
315
|
|
332
|
-
def
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
316
|
+
def build_version_node(xml, service_id, major, intermediate, minor)
|
317
|
+
xml.Version do
|
318
|
+
xml.ServiceId(service_id)
|
319
|
+
xml.Major(major)
|
320
|
+
xml.Intermediate(intermediate)
|
321
|
+
xml.Minor(minor)
|
322
|
+
end
|
323
|
+
end
|
340
324
|
|
341
|
-
|
325
|
+
def build_location_node(xml, name, location)
|
326
|
+
xml.public_send(name) do
|
327
|
+
xml.Address do
|
328
|
+
xml.StreetLines(location.address1) if location.address1
|
329
|
+
xml.StreetLines(location.address2) if location.address2
|
330
|
+
xml.City(location.city) if location.city
|
331
|
+
xml.PostalCode(location.postal_code)
|
332
|
+
xml.CountryCode(location.country_code(:alpha2))
|
333
|
+
xml.Residential(true) unless location.commercial?
|
342
334
|
end
|
343
335
|
end
|
344
336
|
end
|
345
337
|
|
346
338
|
def parse_rate_response(origin, destination, packages, response, options)
|
347
|
-
|
348
|
-
|
349
|
-
xml = build_document(response)
|
350
|
-
root_node = xml.elements['RateReply']
|
339
|
+
xml = build_document(response, 'RateReply')
|
351
340
|
|
352
341
|
success = response_success?(xml)
|
353
342
|
message = response_message(xml)
|
354
343
|
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
service_code = rated_shipment.get_text('ServiceType').to_s
|
359
|
-
is_saturday_delivery = rated_shipment.get_text('AppliedOptions').to_s == 'SATURDAY_DELIVERY'
|
344
|
+
rate_estimates = xml.root.css('> RateReplyDetails').map do |rated_shipment|
|
345
|
+
service_code = rated_shipment.at('ServiceType').text
|
346
|
+
is_saturday_delivery = rated_shipment.at('AppliedOptions').try(:text) == 'SATURDAY_DELIVERY'
|
360
347
|
service_type = is_saturday_delivery ? "#{service_code}_SATURDAY_DELIVERY" : service_code
|
361
348
|
|
362
|
-
transit_time = rated_shipment.
|
363
|
-
max_transit_time = rated_shipment.
|
349
|
+
transit_time = rated_shipment.at('TransitTime').text if service_code == "FEDEX_GROUND"
|
350
|
+
max_transit_time = rated_shipment.at('MaximumTransitTime').try(:text) if service_code == "FEDEX_GROUND"
|
364
351
|
|
365
|
-
delivery_timestamp = rated_shipment.
|
352
|
+
delivery_timestamp = rated_shipment.at('DeliveryTimestamp').try(:text)
|
366
353
|
|
367
354
|
delivery_range = delivery_range_from(transit_time, max_transit_time, delivery_timestamp, options)
|
368
355
|
|
369
|
-
currency = rated_shipment.
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
356
|
+
currency = rated_shipment.at('RatedShipmentDetails/ShipmentRateDetail/TotalNetCharge/Currency').text
|
357
|
+
RateEstimate.new(origin, destination, @@name,
|
358
|
+
self.class.service_name_for_code(service_type),
|
359
|
+
:service_code => service_code,
|
360
|
+
:total_price => rated_shipment.at('RatedShipmentDetails/ShipmentRateDetail/TotalNetCharge/Amount').text.to_f,
|
361
|
+
:currency => currency,
|
362
|
+
:packages => packages,
|
363
|
+
:delivery_range => delivery_range)
|
377
364
|
end
|
378
365
|
|
379
366
|
if rate_estimates.empty?
|
@@ -413,8 +400,7 @@ module ActiveShipping
|
|
413
400
|
end
|
414
401
|
|
415
402
|
def parse_tracking_response(response, options)
|
416
|
-
xml = build_document(response)
|
417
|
-
root_node = xml.elements['TrackReply']
|
403
|
+
xml = build_document(response, 'TrackReply')
|
418
404
|
|
419
405
|
success = response_success?(xml)
|
420
406
|
message = response_message(xml)
|
@@ -424,23 +410,47 @@ module ActiveShipping
|
|
424
410
|
delivery_signature = nil
|
425
411
|
shipment_events = []
|
426
412
|
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
413
|
+
all_tracking_details = xml.root.xpath('CompletedTrackDetails/TrackDetails')
|
414
|
+
tracking_details = case all_tracking_details.length
|
415
|
+
when 1
|
416
|
+
all_tracking_details.first
|
417
|
+
when 0
|
418
|
+
raise ActiveShipping::Error, "The response did not contain tracking details"
|
419
|
+
else
|
420
|
+
all_unique_identifiers = xml.root.xpath('CompletedTrackDetails/TrackDetails/TrackingNumberUniqueIdentifier').map(&:text)
|
421
|
+
raise ActiveShipping::Error, "Multiple matches were found. Specify a unqiue identifier: #{all_unique_identifiers.join(', ')}"
|
422
|
+
end
|
423
|
+
|
432
424
|
|
433
|
-
|
434
|
-
|
425
|
+
first_notification = tracking_details.at('Notification')
|
426
|
+
if first_notification.at('Severity').text == 'ERROR'
|
427
|
+
case first_notification.at('Code').text
|
428
|
+
when '9040'
|
429
|
+
raise ActiveShipping::ShipmentNotFound, first_notification.at('Message').text
|
430
|
+
else
|
431
|
+
raise ActiveShipping::ResponseContentError, first_notification.at('Message').text
|
432
|
+
end
|
435
433
|
end
|
436
434
|
|
437
|
-
|
435
|
+
tracking_number = tracking_details.at('TrackingNumber').text
|
436
|
+
status_detail = tracking_details.at('StatusDetail')
|
437
|
+
if status_detail.nil?
|
438
|
+
raise ActiveShipping::Error, "Tracking response does not contain status information"
|
439
|
+
end
|
440
|
+
|
441
|
+
status_code = status_detail.at('Code').text
|
442
|
+
status_description = (status_detail.at('AncillaryDetails/ReasonDescription') || status_detail.at('Description')).text
|
443
|
+
status = TRACKING_STATUS_CODES[status_code]
|
438
444
|
|
439
|
-
if
|
445
|
+
if status_code == 'DL' && tracking_details.at('AvailableImages').try(:text) == 'SIGNATURE_PROOF_OF_DELIVERY'
|
446
|
+
delivery_signature = tracking_details.at('DeliverySignatureName').text
|
447
|
+
end
|
448
|
+
|
449
|
+
if origin_node = tracking_details.at('OriginLocationAddress')
|
440
450
|
origin = Location.new(
|
441
|
-
:country => origin_node.
|
442
|
-
:province => origin_node.
|
443
|
-
:city => origin_node.
|
451
|
+
:country => origin_node.at('CountryCode').text,
|
452
|
+
:province => origin_node.at('StateOrProvinceCode').text,
|
453
|
+
:city => origin_node.at('City').text
|
444
454
|
)
|
445
455
|
end
|
446
456
|
|
@@ -451,19 +461,19 @@ module ActiveShipping
|
|
451
461
|
actual_delivery_time = extract_timestamp(tracking_details, 'ActualDeliveryTimestamp')
|
452
462
|
scheduled_delivery_time = extract_timestamp(tracking_details, 'EstimatedDeliveryTimestamp')
|
453
463
|
|
454
|
-
tracking_details.
|
455
|
-
address = event.
|
464
|
+
tracking_details.xpath('Events').each do |event|
|
465
|
+
address = event.at('Address')
|
466
|
+
next if address.nil? || address.at('CountryCode').nil?
|
456
467
|
|
457
|
-
city = address.
|
458
|
-
state = address.
|
459
|
-
zip_code = address.
|
460
|
-
country = address.
|
461
|
-
next if country.blank?
|
468
|
+
city = address.at('City').try(:text)
|
469
|
+
state = address.at('StateOrProvinceCode').try(:text)
|
470
|
+
zip_code = address.at('PostalCode').try(:text)
|
471
|
+
country = address.at('CountryCode').try(:text)
|
462
472
|
|
463
473
|
location = Location.new(:city => city, :state => state, :postal_code => zip_code, :country => country)
|
464
|
-
description = event.
|
474
|
+
description = event.at('EventDescription').text
|
465
475
|
|
466
|
-
time = Time.parse(
|
476
|
+
time = Time.parse(event.at('Timestamp').text)
|
467
477
|
zoneless_time = time.utc
|
468
478
|
|
469
479
|
shipment_events << ShipmentEvent.new(description, zoneless_time, location)
|
@@ -501,22 +511,17 @@ module ActiveShipping
|
|
501
511
|
(Time.now + delay_in_hours.hours).to_date
|
502
512
|
end
|
503
513
|
|
504
|
-
def response_status_node(document)
|
505
|
-
document.elements['/*/Notifications/']
|
506
|
-
end
|
507
|
-
|
508
514
|
def response_success?(document)
|
509
|
-
|
510
|
-
return false if
|
511
|
-
|
512
|
-
%w(SUCCESS WARNING NOTE).include? response_node.get_text('Severity').to_s
|
515
|
+
highest_severity = document.root.at('HighestSeverity')
|
516
|
+
return false if highest_severity.nil?
|
517
|
+
%w(SUCCESS WARNING NOTE).include?(highest_severity.text)
|
513
518
|
end
|
514
519
|
|
515
520
|
def response_message(document)
|
516
|
-
|
517
|
-
return "" if
|
521
|
+
notifications = document.root.at('Notifications')
|
522
|
+
return "" if notifications.nil?
|
518
523
|
|
519
|
-
"#{
|
524
|
+
"#{notifications.at('Severity').text} - #{notifications.at('Code').text}: #{notifications.at('Message').text}"
|
520
525
|
end
|
521
526
|
|
522
527
|
def commit(request, test = false)
|
@@ -535,15 +540,15 @@ module ActiveShipping
|
|
535
540
|
def extract_address(document, possible_node_names)
|
536
541
|
node = nil
|
537
542
|
possible_node_names.each do |name|
|
538
|
-
node
|
543
|
+
node = document.at(name)
|
539
544
|
break if node
|
540
545
|
end
|
541
546
|
|
542
|
-
args = if node && node.
|
547
|
+
args = if node && node.at('CountryCode')
|
543
548
|
{
|
544
|
-
:country => node.
|
545
|
-
:province => node.
|
546
|
-
:city => node.
|
549
|
+
:country => node.at('CountryCode').text,
|
550
|
+
:province => node.at('StateOrProvinceCode').text,
|
551
|
+
:city => node.at('City').text
|
547
552
|
}
|
548
553
|
else
|
549
554
|
{
|
@@ -557,22 +562,23 @@ module ActiveShipping
|
|
557
562
|
end
|
558
563
|
|
559
564
|
def extract_timestamp(document, node_name)
|
560
|
-
if timestamp_node = document.
|
561
|
-
|
565
|
+
if timestamp_node = document.at(node_name)
|
566
|
+
if timestamp_node.text =~ /\A(\d{4}-\d{2}-\d{2})T00:00:00\Z/
|
567
|
+
Date.parse($1)
|
568
|
+
else
|
569
|
+
Time.parse(timestamp_node.text)
|
570
|
+
end
|
562
571
|
end
|
563
572
|
end
|
564
573
|
|
565
|
-
def
|
566
|
-
|
567
|
-
|
568
|
-
|
569
|
-
xml
|
574
|
+
def build_document(xml, expected_root_tag)
|
575
|
+
document = Nokogiri.XML(xml) { |config| config.strict }
|
576
|
+
document.remove_namespaces!
|
577
|
+
if document.root.nil? || document.root.name != expected_root_tag
|
578
|
+
raise ActiveShipping::ResponseContentError.new(StandardError.new('Invalid document'), xml)
|
570
579
|
end
|
571
|
-
|
572
|
-
|
573
|
-
def build_document(xml)
|
574
|
-
REXML::Document.new(xml)
|
575
|
-
rescue REXML::ParseException => e
|
580
|
+
document
|
581
|
+
rescue Nokogiri::XML::SyntaxError => e
|
576
582
|
raise ActiveShipping::ResponseContentError.new(e, xml)
|
577
583
|
end
|
578
584
|
end
|