active_shipping 0.12.6 → 1.0.0.pre1
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 +2 -0
- data.tar.gz.sig +0 -0
- data/{CHANGELOG → CHANGELOG.md} +6 -2
- data/CONTRIBUTING.md +32 -0
- data/{README.markdown → README.md} +45 -61
- data/lib/active_shipping.rb +20 -28
- data/lib/active_shipping/carrier.rb +82 -0
- data/lib/active_shipping/carriers.rb +33 -0
- data/lib/active_shipping/carriers/benchmark_carrier.rb +31 -0
- data/lib/active_shipping/carriers/bogus_carrier.rb +12 -0
- data/lib/active_shipping/carriers/canada_post.rb +253 -0
- data/lib/active_shipping/carriers/canada_post_pws.rb +870 -0
- data/lib/active_shipping/carriers/fedex.rb +579 -0
- data/lib/active_shipping/carriers/kunaki.rb +164 -0
- data/lib/active_shipping/carriers/new_zealand_post.rb +262 -0
- data/lib/active_shipping/carriers/shipwire.rb +181 -0
- data/lib/active_shipping/carriers/stamps.rb +861 -0
- data/lib/active_shipping/carriers/ups.rb +648 -0
- data/lib/active_shipping/carriers/usps.rb +642 -0
- data/lib/active_shipping/errors.rb +7 -0
- data/lib/active_shipping/label_response.rb +23 -0
- data/lib/active_shipping/location.rb +149 -0
- data/lib/active_shipping/package.rb +241 -0
- data/lib/active_shipping/rate_estimate.rb +64 -0
- data/lib/active_shipping/rate_response.rb +13 -0
- data/lib/active_shipping/response.rb +41 -0
- data/lib/active_shipping/shipment_event.rb +17 -0
- data/lib/active_shipping/shipment_packer.rb +73 -0
- data/lib/active_shipping/shipping_response.rb +12 -0
- data/lib/active_shipping/tracking_response.rb +52 -0
- data/lib/active_shipping/version.rb +1 -1
- data/lib/vendor/quantified/test/length_test.rb +2 -2
- data/lib/vendor/xml_node/test/test_parsing.rb +1 -1
- metadata +58 -36
- metadata.gz.sig +0 -0
- data/lib/active_shipping/shipping/base.rb +0 -13
- data/lib/active_shipping/shipping/carrier.rb +0 -84
- data/lib/active_shipping/shipping/carriers.rb +0 -23
- data/lib/active_shipping/shipping/carriers/benchmark_carrier.rb +0 -33
- data/lib/active_shipping/shipping/carriers/bogus_carrier.rb +0 -14
- data/lib/active_shipping/shipping/carriers/canada_post.rb +0 -257
- data/lib/active_shipping/shipping/carriers/canada_post_pws.rb +0 -874
- data/lib/active_shipping/shipping/carriers/fedex.rb +0 -581
- data/lib/active_shipping/shipping/carriers/kunaki.rb +0 -166
- data/lib/active_shipping/shipping/carriers/new_zealand_post.rb +0 -262
- data/lib/active_shipping/shipping/carriers/shipwire.rb +0 -184
- data/lib/active_shipping/shipping/carriers/stamps.rb +0 -864
- data/lib/active_shipping/shipping/carriers/ups.rb +0 -650
- data/lib/active_shipping/shipping/carriers/usps.rb +0 -649
- data/lib/active_shipping/shipping/errors.rb +0 -9
- data/lib/active_shipping/shipping/label_response.rb +0 -25
- data/lib/active_shipping/shipping/location.rb +0 -152
- data/lib/active_shipping/shipping/package.rb +0 -243
- data/lib/active_shipping/shipping/rate_estimate.rb +0 -66
- data/lib/active_shipping/shipping/rate_response.rb +0 -15
- data/lib/active_shipping/shipping/response.rb +0 -43
- data/lib/active_shipping/shipping/shipment_event.rb +0 -19
- data/lib/active_shipping/shipping/shipment_packer.rb +0 -75
- data/lib/active_shipping/shipping/shipping_response.rb +0 -14
- data/lib/active_shipping/shipping/tracking_response.rb +0 -54
@@ -0,0 +1,579 @@
|
|
1
|
+
# FedEx module by Jimmy Baker
|
2
|
+
# http://github.com/jimmyebaker
|
3
|
+
|
4
|
+
require 'date'
|
5
|
+
module ActiveShipping
|
6
|
+
# :key is your developer API key
|
7
|
+
# :password is your API password
|
8
|
+
# :account is your FedEx account number
|
9
|
+
# :login is your meter number
|
10
|
+
class FedEx < Carrier
|
11
|
+
self.retry_safe = true
|
12
|
+
|
13
|
+
cattr_reader :name
|
14
|
+
@@name = "FedEx"
|
15
|
+
|
16
|
+
TEST_URL = 'https://gatewaybeta.fedex.com:443/xml'
|
17
|
+
LIVE_URL = 'https://gateway.fedex.com:443/xml'
|
18
|
+
|
19
|
+
CARRIER_CODES = {
|
20
|
+
"fedex_ground" => "FDXG",
|
21
|
+
"fedex_express" => "FDXE"
|
22
|
+
}
|
23
|
+
|
24
|
+
DELIVERY_ADDRESS_NODE_NAMES = %w(DestinationAddress ActualDeliveryAddress)
|
25
|
+
SHIPPER_ADDRESS_NODE_NAMES = %w(ShipperAddress)
|
26
|
+
|
27
|
+
SERVICE_TYPES = {
|
28
|
+
"PRIORITY_OVERNIGHT" => "FedEx Priority Overnight",
|
29
|
+
"PRIORITY_OVERNIGHT_SATURDAY_DELIVERY" => "FedEx Priority Overnight Saturday Delivery",
|
30
|
+
"FEDEX_2_DAY" => "FedEx 2 Day",
|
31
|
+
"FEDEX_2_DAY_SATURDAY_DELIVERY" => "FedEx 2 Day Saturday Delivery",
|
32
|
+
"STANDARD_OVERNIGHT" => "FedEx Standard Overnight",
|
33
|
+
"FIRST_OVERNIGHT" => "FedEx First Overnight",
|
34
|
+
"FIRST_OVERNIGHT_SATURDAY_DELIVERY" => "FedEx First Overnight Saturday Delivery",
|
35
|
+
"FEDEX_EXPRESS_SAVER" => "FedEx Express Saver",
|
36
|
+
"FEDEX_1_DAY_FREIGHT" => "FedEx 1 Day Freight",
|
37
|
+
"FEDEX_1_DAY_FREIGHT_SATURDAY_DELIVERY" => "FedEx 1 Day Freight Saturday Delivery",
|
38
|
+
"FEDEX_2_DAY_FREIGHT" => "FedEx 2 Day Freight",
|
39
|
+
"FEDEX_2_DAY_FREIGHT_SATURDAY_DELIVERY" => "FedEx 2 Day Freight Saturday Delivery",
|
40
|
+
"FEDEX_3_DAY_FREIGHT" => "FedEx 3 Day Freight",
|
41
|
+
"FEDEX_3_DAY_FREIGHT_SATURDAY_DELIVERY" => "FedEx 3 Day Freight Saturday Delivery",
|
42
|
+
"INTERNATIONAL_PRIORITY" => "FedEx International Priority",
|
43
|
+
"INTERNATIONAL_PRIORITY_SATURDAY_DELIVERY" => "FedEx International Priority Saturday Delivery",
|
44
|
+
"INTERNATIONAL_ECONOMY" => "FedEx International Economy",
|
45
|
+
"INTERNATIONAL_FIRST" => "FedEx International First",
|
46
|
+
"INTERNATIONAL_PRIORITY_FREIGHT" => "FedEx International Priority Freight",
|
47
|
+
"INTERNATIONAL_ECONOMY_FREIGHT" => "FedEx International Economy Freight",
|
48
|
+
"GROUND_HOME_DELIVERY" => "FedEx Ground Home Delivery",
|
49
|
+
"FEDEX_GROUND" => "FedEx Ground",
|
50
|
+
"INTERNATIONAL_GROUND" => "FedEx International Ground",
|
51
|
+
"SMART_POST" => "FedEx SmartPost",
|
52
|
+
"FEDEX_FREIGHT_PRIORITY" => "FedEx Freight Priority",
|
53
|
+
"FEDEX_FREIGHT_ECONOMY" => "FedEx Freight Economy"
|
54
|
+
}
|
55
|
+
|
56
|
+
PACKAGE_TYPES = {
|
57
|
+
"fedex_envelope" => "FEDEX_ENVELOPE",
|
58
|
+
"fedex_pak" => "FEDEX_PAK",
|
59
|
+
"fedex_box" => "FEDEX_BOX",
|
60
|
+
"fedex_tube" => "FEDEX_TUBE",
|
61
|
+
"fedex_10_kg_box" => "FEDEX_10KG_BOX",
|
62
|
+
"fedex_25_kg_box" => "FEDEX_25KG_BOX",
|
63
|
+
"your_packaging" => "YOUR_PACKAGING"
|
64
|
+
}
|
65
|
+
|
66
|
+
DROPOFF_TYPES = {
|
67
|
+
'regular_pickup' => 'REGULAR_PICKUP',
|
68
|
+
'request_courier' => 'REQUEST_COURIER',
|
69
|
+
'dropbox' => 'DROP_BOX',
|
70
|
+
'business_service_center' => 'BUSINESS_SERVICE_CENTER',
|
71
|
+
'station' => 'STATION'
|
72
|
+
}
|
73
|
+
|
74
|
+
PAYMENT_TYPES = {
|
75
|
+
'sender' => 'SENDER',
|
76
|
+
'recipient' => 'RECIPIENT',
|
77
|
+
'third_party' => 'THIRDPARTY',
|
78
|
+
'collect' => 'COLLECT'
|
79
|
+
}
|
80
|
+
|
81
|
+
PACKAGE_IDENTIFIER_TYPES = {
|
82
|
+
'tracking_number' => 'TRACKING_NUMBER_OR_DOORTAG',
|
83
|
+
'door_tag' => 'TRACKING_NUMBER_OR_DOORTAG',
|
84
|
+
'rma' => 'RMA',
|
85
|
+
'ground_shipment_id' => 'GROUND_SHIPMENT_ID',
|
86
|
+
'ground_invoice_number' => 'GROUND_INVOICE_NUMBER',
|
87
|
+
'ground_customer_reference' => 'GROUND_CUSTOMER_REFERENCE',
|
88
|
+
'ground_po' => 'GROUND_PO',
|
89
|
+
'express_reference' => 'EXPRESS_REFERENCE',
|
90
|
+
'express_mps_master' => 'EXPRESS_MPS_MASTER'
|
91
|
+
}
|
92
|
+
|
93
|
+
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)
|
94
|
+
|
95
|
+
# FedEx tracking codes as described in the FedEx Tracking Service WSDL Guide
|
96
|
+
# All delays also have been marked as exceptions
|
97
|
+
TRACKING_STATUS_CODES = HashWithIndifferentAccess.new(
|
98
|
+
'AA' => :at_airport,
|
99
|
+
'AD' => :at_delivery,
|
100
|
+
'AF' => :at_fedex_facility,
|
101
|
+
'AR' => :at_fedex_facility,
|
102
|
+
'AP' => :at_pickup,
|
103
|
+
'CA' => :canceled,
|
104
|
+
'CH' => :location_changed,
|
105
|
+
'DE' => :exception,
|
106
|
+
'DL' => :delivered,
|
107
|
+
'DP' => :departed_fedex_location,
|
108
|
+
'DR' => :vehicle_furnished_not_used,
|
109
|
+
'DS' => :vehicle_dispatched,
|
110
|
+
'DY' => :exception,
|
111
|
+
'EA' => :exception,
|
112
|
+
'ED' => :enroute_to_delivery,
|
113
|
+
'EO' => :enroute_to_origin_airport,
|
114
|
+
'EP' => :enroute_to_pickup,
|
115
|
+
'FD' => :at_fedex_destination,
|
116
|
+
'HL' => :held_at_location,
|
117
|
+
'IT' => :in_transit,
|
118
|
+
'LO' => :left_origin,
|
119
|
+
'OC' => :order_created,
|
120
|
+
'OD' => :out_for_delivery,
|
121
|
+
'PF' => :plane_in_flight,
|
122
|
+
'PL' => :plane_landed,
|
123
|
+
'PU' => :picked_up,
|
124
|
+
'RS' => :return_to_shipper,
|
125
|
+
'SE' => :exception,
|
126
|
+
'SF' => :at_sort_facility,
|
127
|
+
'SP' => :split_status,
|
128
|
+
'TR' => :transfer
|
129
|
+
)
|
130
|
+
|
131
|
+
def self.service_name_for_code(service_code)
|
132
|
+
SERVICE_TYPES[service_code] || "FedEx #{service_code.titleize.sub(/Fedex /, '')}"
|
133
|
+
end
|
134
|
+
|
135
|
+
def requirements
|
136
|
+
[:key, :password, :account, :login]
|
137
|
+
end
|
138
|
+
|
139
|
+
def find_rates(origin, destination, packages, options = {})
|
140
|
+
options = @options.update(options)
|
141
|
+
packages = Array(packages)
|
142
|
+
|
143
|
+
rate_request = build_rate_request(origin, destination, packages, options)
|
144
|
+
|
145
|
+
xml = commit(save_request(rate_request), (options[:test] || false))
|
146
|
+
response = remove_version_prefix(xml)
|
147
|
+
|
148
|
+
parse_rate_response(origin, destination, packages, response, options)
|
149
|
+
end
|
150
|
+
|
151
|
+
def find_tracking_info(tracking_number, options = {})
|
152
|
+
options = @options.update(options)
|
153
|
+
|
154
|
+
tracking_request = build_tracking_request(tracking_number, options)
|
155
|
+
xml = commit(save_request(tracking_request), (options[:test] || false))
|
156
|
+
response = remove_version_prefix(xml)
|
157
|
+
parse_tracking_response(response, options)
|
158
|
+
end
|
159
|
+
|
160
|
+
protected
|
161
|
+
|
162
|
+
def build_rate_request(origin, destination, packages, options = {})
|
163
|
+
imperial = %w(US LR MM).include?(origin.country_code(:alpha2))
|
164
|
+
|
165
|
+
xml_request = XmlNode.new('RateRequest', 'xmlns' => 'http://fedex.com/ws/rate/v13') do |root_node|
|
166
|
+
root_node << build_request_header
|
167
|
+
root_node << build_version_node
|
168
|
+
|
169
|
+
# Returns delivery dates
|
170
|
+
root_node << XmlNode.new('ReturnTransitAndCommit', true)
|
171
|
+
# Returns saturday delivery shipping options when available
|
172
|
+
root_node << XmlNode.new('VariableOptions', 'SATURDAY_DELIVERY')
|
173
|
+
|
174
|
+
root_node << XmlNode.new('RequestedShipment') do |rs|
|
175
|
+
rs << XmlNode.new('ShipTimestamp', ship_timestamp(options[:turn_around_time]))
|
176
|
+
|
177
|
+
freight = has_freight?(options)
|
178
|
+
|
179
|
+
unless freight
|
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
|
190
|
+
|
191
|
+
if freight
|
192
|
+
# build xml for freight rate requests
|
193
|
+
freight_options = options[:freight]
|
194
|
+
rs << build_shipping_charges_payment_node(freight_options)
|
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
|
202
|
+
end
|
203
|
+
|
204
|
+
rs << build_rate_request_types_node
|
205
|
+
rs << XmlNode.new('PackageCount', packages.size)
|
206
|
+
rs << build_packages_nodes(packages, imperial)
|
207
|
+
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
211
|
+
xml_request.to_s
|
212
|
+
end
|
213
|
+
|
214
|
+
def build_packages_nodes(packages, imperial)
|
215
|
+
packages.map do |pkg|
|
216
|
+
XmlNode.new('RequestedPackageLineItems') do |rps|
|
217
|
+
rps << XmlNode.new('GroupPackageCount', 1)
|
218
|
+
rps << build_package_weight_node(pkg, imperial)
|
219
|
+
rps << build_package_dimensions_node(pkg, imperial)
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
def build_shipping_charges_payment_node(freight_options)
|
225
|
+
XmlNode.new('ShippingChargesPayment') do |shipping_charges_payment|
|
226
|
+
shipping_charges_payment << XmlNode.new('PaymentType', freight_options[:payment_type])
|
227
|
+
shipping_charges_payment << XmlNode.new('Payor') do |payor|
|
228
|
+
payor << XmlNode.new('ResponsibleParty') do |responsible_party|
|
229
|
+
# TODO: case of different freight account numbers?
|
230
|
+
responsible_party << XmlNode.new('AccountNumber', freight_options[:account])
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
def build_freight_shipment_detail_node(freight_options, packages, imperial)
|
237
|
+
XmlNode.new('FreightShipmentDetail') do |freight_shipment_detail|
|
238
|
+
# TODO: case of different freight account numbers?
|
239
|
+
freight_shipment_detail << XmlNode.new('FedExFreightAccountNumber', freight_options[:account])
|
240
|
+
freight_shipment_detail << build_location_node('FedExFreightBillingContactAndAddress', freight_options[:billing_location])
|
241
|
+
freight_shipment_detail << XmlNode.new('Role', freight_options[:role])
|
242
|
+
|
243
|
+
packages.each do |pkg|
|
244
|
+
freight_shipment_detail << XmlNode.new('LineItems') do |line_items|
|
245
|
+
line_items << XmlNode.new('FreightClass', freight_options[:freight_class])
|
246
|
+
line_items << XmlNode.new('Packaging', freight_options[:packaging])
|
247
|
+
line_items << build_package_weight_node(pkg, imperial)
|
248
|
+
line_items << build_package_dimensions_node(pkg, imperial)
|
249
|
+
end
|
250
|
+
end
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
def has_freight?(options)
|
255
|
+
options[:freight] && options[:freight].present?
|
256
|
+
end
|
257
|
+
|
258
|
+
def build_package_weight_node(pkg, imperial)
|
259
|
+
XmlNode.new('Weight') do |tw|
|
260
|
+
tw << XmlNode.new('Units', imperial ? 'LB' : 'KG')
|
261
|
+
tw << XmlNode.new('Value', [((imperial ? pkg.lbs : pkg.kgs).to_f * 1000).round / 1000.0, 0.1].max)
|
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')
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
def build_package_dimensions_node(pkg, imperial)
|
275
|
+
XmlNode.new('Dimensions') do |dimensions|
|
276
|
+
[:length, :width, :height].each do |axis|
|
277
|
+
value = ((imperial ? pkg.inches(axis) : pkg.cm(axis)).to_f * 1000).round / 1000.0 # 3 decimals
|
278
|
+
dimensions << XmlNode.new(axis.to_s.capitalize, value.ceil)
|
279
|
+
end
|
280
|
+
dimensions << XmlNode.new('Units', imperial ? 'IN' : 'CM')
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
284
|
+
def build_rate_request_types_node(type = 'ACCOUNT')
|
285
|
+
XmlNode.new('RateRequestTypes', type)
|
286
|
+
end
|
287
|
+
|
288
|
+
def build_tracking_request(tracking_number, options = {})
|
289
|
+
xml_request = XmlNode.new('TrackRequest', 'xmlns' => 'http://fedex.com/ws/track/v3') do |root_node|
|
290
|
+
root_node << build_request_header
|
291
|
+
|
292
|
+
# Version
|
293
|
+
root_node << XmlNode.new('Version') do |version_node|
|
294
|
+
version_node << XmlNode.new('ServiceId', 'trck')
|
295
|
+
version_node << XmlNode.new('Major', '3')
|
296
|
+
version_node << XmlNode.new('Intermediate', '0')
|
297
|
+
version_node << XmlNode.new('Minor', '0')
|
298
|
+
end
|
299
|
+
|
300
|
+
root_node << XmlNode.new('PackageIdentifier') do |package_node|
|
301
|
+
package_node << XmlNode.new('Value', tracking_number)
|
302
|
+
package_node << XmlNode.new('Type', PACKAGE_IDENTIFIER_TYPES[options['package_identifier_type'] || 'tracking_number'])
|
303
|
+
end
|
304
|
+
|
305
|
+
root_node << XmlNode.new('ShipDateRangeBegin', options['ship_date_range_begin']) if options['ship_date_range_begin']
|
306
|
+
root_node << XmlNode.new('ShipDateRangeEnd', options['ship_date_range_end']) if options['ship_date_range_end']
|
307
|
+
root_node << XmlNode.new('IncludeDetailedScans', 1)
|
308
|
+
end
|
309
|
+
xml_request.to_s
|
310
|
+
end
|
311
|
+
|
312
|
+
def build_request_header
|
313
|
+
web_authentication_detail = XmlNode.new('WebAuthenticationDetail') do |wad|
|
314
|
+
wad << XmlNode.new('UserCredential') do |uc|
|
315
|
+
uc << XmlNode.new('Key', @options[:key])
|
316
|
+
uc << XmlNode.new('Password', @options[:password])
|
317
|
+
end
|
318
|
+
end
|
319
|
+
|
320
|
+
client_detail = XmlNode.new('ClientDetail') do |cd|
|
321
|
+
cd << XmlNode.new('AccountNumber', @options[:account])
|
322
|
+
cd << XmlNode.new('MeterNumber', @options[:login])
|
323
|
+
end
|
324
|
+
|
325
|
+
trasaction_detail = XmlNode.new('TransactionDetail') do |td|
|
326
|
+
td << XmlNode.new('CustomerTransactionId', @options[:transaction_id] || 'ActiveShipping') # TODO: Need to do something better with this..
|
327
|
+
end
|
328
|
+
|
329
|
+
[web_authentication_detail, client_detail, trasaction_detail]
|
330
|
+
end
|
331
|
+
|
332
|
+
def build_location_node(name, location)
|
333
|
+
XmlNode.new(name) do |xml_node|
|
334
|
+
xml_node << XmlNode.new('Address') do |address_node|
|
335
|
+
address_node << XmlNode.new('StreetLines', location.address1) if location.address1
|
336
|
+
address_node << XmlNode.new('StreetLines', location.address2) if location.address2
|
337
|
+
address_node << XmlNode.new('City', location.city) if location.city
|
338
|
+
address_node << XmlNode.new('PostalCode', location.postal_code)
|
339
|
+
address_node << XmlNode.new("CountryCode", location.country_code(:alpha2))
|
340
|
+
|
341
|
+
address_node << XmlNode.new("Residential", true) unless location.commercial?
|
342
|
+
end
|
343
|
+
end
|
344
|
+
end
|
345
|
+
|
346
|
+
def parse_rate_response(origin, destination, packages, response, options)
|
347
|
+
rate_estimates = []
|
348
|
+
|
349
|
+
xml = build_document(response)
|
350
|
+
root_node = xml.elements['RateReply']
|
351
|
+
|
352
|
+
success = response_success?(xml)
|
353
|
+
message = response_message(xml)
|
354
|
+
|
355
|
+
raise ActiveShipping::ResponseContentError.new(StandardError.new('Invalid document'), xml) unless root_node
|
356
|
+
|
357
|
+
root_node.elements.each('RateReplyDetails') do |rated_shipment|
|
358
|
+
service_code = rated_shipment.get_text('ServiceType').to_s
|
359
|
+
is_saturday_delivery = rated_shipment.get_text('AppliedOptions').to_s == 'SATURDAY_DELIVERY'
|
360
|
+
service_type = is_saturday_delivery ? "#{service_code}_SATURDAY_DELIVERY" : service_code
|
361
|
+
|
362
|
+
transit_time = rated_shipment.get_text('TransitTime').to_s if service_code == "FEDEX_GROUND"
|
363
|
+
max_transit_time = rated_shipment.get_text('MaximumTransitTime').to_s if service_code == "FEDEX_GROUND"
|
364
|
+
|
365
|
+
delivery_timestamp = rated_shipment.get_text('DeliveryTimestamp').to_s
|
366
|
+
|
367
|
+
delivery_range = delivery_range_from(transit_time, max_transit_time, delivery_timestamp, options)
|
368
|
+
|
369
|
+
currency = rated_shipment.get_text('RatedShipmentDetails/ShipmentRateDetail/TotalNetCharge/Currency').to_s
|
370
|
+
rate_estimates << RateEstimate.new(origin, destination, @@name,
|
371
|
+
self.class.service_name_for_code(service_type),
|
372
|
+
:service_code => service_code,
|
373
|
+
:total_price => rated_shipment.get_text('RatedShipmentDetails/ShipmentRateDetail/TotalNetCharge/Amount').to_s.to_f,
|
374
|
+
:currency => currency,
|
375
|
+
:packages => packages,
|
376
|
+
:delivery_range => delivery_range)
|
377
|
+
end
|
378
|
+
|
379
|
+
if rate_estimates.empty?
|
380
|
+
success = false
|
381
|
+
message = "No shipping rates could be found for the destination address" if message.blank?
|
382
|
+
end
|
383
|
+
|
384
|
+
RateResponse.new(success, message, Hash.from_xml(response), :rates => rate_estimates, :xml => response, :request => last_request, :log_xml => options[:log_xml])
|
385
|
+
end
|
386
|
+
|
387
|
+
def delivery_range_from(transit_time, max_transit_time, delivery_timestamp, options)
|
388
|
+
delivery_range = [delivery_timestamp, delivery_timestamp]
|
389
|
+
|
390
|
+
# if there's no delivery timestamp but we do have a transit time, use it
|
391
|
+
if delivery_timestamp.blank? && transit_time.present?
|
392
|
+
transit_range = parse_transit_times([transit_time, max_transit_time.presence || transit_time])
|
393
|
+
delivery_range = transit_range.map { |days| business_days_from(ship_date(options[:turn_around_time]), days) }
|
394
|
+
end
|
395
|
+
|
396
|
+
delivery_range
|
397
|
+
end
|
398
|
+
|
399
|
+
def business_days_from(date, days)
|
400
|
+
future_date = date
|
401
|
+
count = 0
|
402
|
+
|
403
|
+
while count < days
|
404
|
+
future_date += 1.day
|
405
|
+
count += 1 if business_day?(future_date)
|
406
|
+
end
|
407
|
+
|
408
|
+
future_date
|
409
|
+
end
|
410
|
+
|
411
|
+
def business_day?(date)
|
412
|
+
(1..5).include?(date.wday)
|
413
|
+
end
|
414
|
+
|
415
|
+
def parse_tracking_response(response, options)
|
416
|
+
xml = build_document(response)
|
417
|
+
root_node = xml.elements['TrackReply']
|
418
|
+
|
419
|
+
success = response_success?(xml)
|
420
|
+
message = response_message(xml)
|
421
|
+
|
422
|
+
if success
|
423
|
+
origin = nil
|
424
|
+
delivery_signature = nil
|
425
|
+
shipment_events = []
|
426
|
+
|
427
|
+
tracking_details = root_node.elements['TrackDetails']
|
428
|
+
tracking_number = tracking_details.get_text('TrackingNumber').to_s
|
429
|
+
status_code = tracking_details.get_text('StatusCode').to_s
|
430
|
+
status_description = tracking_details.get_text('StatusDescription').to_s
|
431
|
+
status = TRACKING_STATUS_CODES[status_code]
|
432
|
+
|
433
|
+
if status_code == 'DL' && tracking_details.get_text('SignatureProofOfDeliveryAvailable').to_s == 'true'
|
434
|
+
delivery_signature = tracking_details.get_text('DeliverySignatureName').to_s
|
435
|
+
end
|
436
|
+
|
437
|
+
origin_node = tracking_details.elements['OriginLocationAddress']
|
438
|
+
|
439
|
+
if origin_node
|
440
|
+
origin = Location.new(
|
441
|
+
:country => origin_node.get_text('CountryCode').to_s,
|
442
|
+
:province => origin_node.get_text('StateOrProvinceCode').to_s,
|
443
|
+
:city => origin_node.get_text('City').to_s
|
444
|
+
)
|
445
|
+
end
|
446
|
+
|
447
|
+
destination = extract_address(tracking_details, DELIVERY_ADDRESS_NODE_NAMES)
|
448
|
+
shipper_address = extract_address(tracking_details, SHIPPER_ADDRESS_NODE_NAMES)
|
449
|
+
|
450
|
+
ship_time = extract_timestamp(tracking_details, 'ShipTimestamp')
|
451
|
+
actual_delivery_time = extract_timestamp(tracking_details, 'ActualDeliveryTimestamp')
|
452
|
+
scheduled_delivery_time = extract_timestamp(tracking_details, 'EstimatedDeliveryTimestamp')
|
453
|
+
|
454
|
+
tracking_details.elements.each('Events') do |event|
|
455
|
+
address = event.elements['Address']
|
456
|
+
|
457
|
+
city = address.get_text('City').to_s
|
458
|
+
state = address.get_text('StateOrProvinceCode').to_s
|
459
|
+
zip_code = address.get_text('PostalCode').to_s
|
460
|
+
country = address.get_text('CountryCode').to_s
|
461
|
+
next if country.blank?
|
462
|
+
|
463
|
+
location = Location.new(:city => city, :state => state, :postal_code => zip_code, :country => country)
|
464
|
+
description = event.get_text('EventDescription').to_s
|
465
|
+
|
466
|
+
time = Time.parse("#{event.get_text('Timestamp').to_s}")
|
467
|
+
zoneless_time = time.utc
|
468
|
+
|
469
|
+
shipment_events << ShipmentEvent.new(description, zoneless_time, location)
|
470
|
+
end
|
471
|
+
shipment_events = shipment_events.sort_by(&:time)
|
472
|
+
|
473
|
+
end
|
474
|
+
|
475
|
+
TrackingResponse.new(success, message, Hash.from_xml(response),
|
476
|
+
:carrier => @@name,
|
477
|
+
:xml => response,
|
478
|
+
:request => last_request,
|
479
|
+
:status => status,
|
480
|
+
:status_code => status_code,
|
481
|
+
:status_description => status_description,
|
482
|
+
:ship_time => ship_time,
|
483
|
+
:scheduled_delivery_date => scheduled_delivery_time,
|
484
|
+
:actual_delivery_date => actual_delivery_time,
|
485
|
+
:delivery_signature => delivery_signature,
|
486
|
+
:shipment_events => shipment_events,
|
487
|
+
:shipper_address => (shipper_address.nil? || shipper_address.unknown?) ? nil : shipper_address,
|
488
|
+
:origin => origin,
|
489
|
+
:destination => destination,
|
490
|
+
:tracking_number => tracking_number
|
491
|
+
)
|
492
|
+
end
|
493
|
+
|
494
|
+
def ship_timestamp(delay_in_hours)
|
495
|
+
delay_in_hours ||= 0
|
496
|
+
Time.now + delay_in_hours.hours
|
497
|
+
end
|
498
|
+
|
499
|
+
def ship_date(delay_in_hours)
|
500
|
+
delay_in_hours ||= 0
|
501
|
+
(Time.now + delay_in_hours.hours).to_date
|
502
|
+
end
|
503
|
+
|
504
|
+
def response_status_node(document)
|
505
|
+
document.elements['/*/Notifications/']
|
506
|
+
end
|
507
|
+
|
508
|
+
def response_success?(document)
|
509
|
+
response_node = response_status_node(document)
|
510
|
+
return false if response_node.nil?
|
511
|
+
|
512
|
+
%w(SUCCESS WARNING NOTE).include? response_node.get_text('Severity').to_s
|
513
|
+
end
|
514
|
+
|
515
|
+
def response_message(document)
|
516
|
+
response_node = response_status_node(document)
|
517
|
+
return "" if response_node.nil?
|
518
|
+
|
519
|
+
"#{response_node.get_text('Severity')} - #{response_node.get_text('Code')}: #{response_node.get_text('Message')}"
|
520
|
+
end
|
521
|
+
|
522
|
+
def commit(request, test = false)
|
523
|
+
ssl_post(test ? TEST_URL : LIVE_URL, request.gsub("\n", ''))
|
524
|
+
end
|
525
|
+
|
526
|
+
def parse_transit_times(times)
|
527
|
+
results = []
|
528
|
+
times.each do |day_count|
|
529
|
+
days = TRANSIT_TIMES.index(day_count.to_s.chomp)
|
530
|
+
results << days.to_i
|
531
|
+
end
|
532
|
+
results
|
533
|
+
end
|
534
|
+
|
535
|
+
def extract_address(document, possible_node_names)
|
536
|
+
node = nil
|
537
|
+
possible_node_names.each do |name|
|
538
|
+
node ||= document.elements[name]
|
539
|
+
break if node
|
540
|
+
end
|
541
|
+
|
542
|
+
args = if node && node.elements['CountryCode']
|
543
|
+
{
|
544
|
+
:country => node.get_text('CountryCode').to_s,
|
545
|
+
:province => node.get_text('StateOrProvinceCode').to_s,
|
546
|
+
:city => node.get_text('City').to_s
|
547
|
+
}
|
548
|
+
else
|
549
|
+
{
|
550
|
+
:country => ActiveUtils::Country.new(:alpha2 => 'ZZ', :name => 'Unknown or Invalid Territory', :alpha3 => 'ZZZ', :numeric => '999'),
|
551
|
+
:province => 'unknown',
|
552
|
+
:city => 'unknown'
|
553
|
+
}
|
554
|
+
end
|
555
|
+
|
556
|
+
Location.new(args)
|
557
|
+
end
|
558
|
+
|
559
|
+
def extract_timestamp(document, node_name)
|
560
|
+
if timestamp_node = document.elements[node_name]
|
561
|
+
Time.parse(timestamp_node.to_s).utc
|
562
|
+
end
|
563
|
+
end
|
564
|
+
|
565
|
+
def remove_version_prefix(xml)
|
566
|
+
if xml =~ /xmlns:v[0-9]/
|
567
|
+
xml.gsub(/<(\/)?.*?\:(.*?)>/, '<\1\2>')
|
568
|
+
else
|
569
|
+
xml
|
570
|
+
end
|
571
|
+
end
|
572
|
+
|
573
|
+
def build_document(xml)
|
574
|
+
REXML::Document.new(xml)
|
575
|
+
rescue REXML::ParseException => e
|
576
|
+
raise ActiveShipping::ResponseContentError.new(e, xml)
|
577
|
+
end
|
578
|
+
end
|
579
|
+
end
|