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