active_shipping 1.0.0.pre4 → 1.0.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.
- 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
|