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,23 +0,0 @@
|
|
1
|
-
require 'active_shipping/shipping/carriers/benchmark_carrier'
|
2
|
-
require 'active_shipping/shipping/carriers/bogus_carrier'
|
3
|
-
require 'active_shipping/shipping/carriers/ups'
|
4
|
-
require 'active_shipping/shipping/carriers/usps'
|
5
|
-
require 'active_shipping/shipping/carriers/fedex'
|
6
|
-
require 'active_shipping/shipping/carriers/shipwire'
|
7
|
-
require 'active_shipping/shipping/carriers/kunaki'
|
8
|
-
require 'active_shipping/shipping/carriers/canada_post'
|
9
|
-
require 'active_shipping/shipping/carriers/new_zealand_post'
|
10
|
-
require 'active_shipping/shipping/carriers/canada_post_pws'
|
11
|
-
require 'active_shipping/shipping/carriers/stamps'
|
12
|
-
|
13
|
-
module ActiveMerchant
|
14
|
-
module Shipping
|
15
|
-
module Carriers
|
16
|
-
class <<self
|
17
|
-
def all
|
18
|
-
[BenchmarkCarrier, BogusCarrier, UPS, USPS, FedEx, Shipwire, Kunaki, CanadaPost, NewZealandPost, CanadaPostPWS, Stamps]
|
19
|
-
end
|
20
|
-
end
|
21
|
-
end
|
22
|
-
end
|
23
|
-
end
|
@@ -1,33 +0,0 @@
|
|
1
|
-
# The time a 3rd-party shipping provider takes to respond to a request varies greatly.
|
2
|
-
# This class simulates these unpredictable delays in shipping rate retrieval so that
|
3
|
-
# load-testing tools run into situations that more accurately reflect the real world.
|
4
|
-
|
5
|
-
module ActiveMerchant
|
6
|
-
module Shipping
|
7
|
-
class BenchmarkCarrier < Carrier
|
8
|
-
cattr_reader :name
|
9
|
-
@@name = "Benchmark Carrier"
|
10
|
-
|
11
|
-
def find_rates(origin, destination, packages, options = {})
|
12
|
-
origin = Location.from(origin)
|
13
|
-
destination = Location.from(destination)
|
14
|
-
packages = Array(packages)
|
15
|
-
|
16
|
-
delay_time = generate_simulated_lag
|
17
|
-
|
18
|
-
bogus_estimate = RateEstimate.new(
|
19
|
-
origin, destination, @@name,
|
20
|
-
"Free Benchmark Shipping", :total_price => 0, :currency => 'USD',
|
21
|
-
:packages => packages, :delivery_range => [Time.now.utc.strftime("%Y-%d-%m"), Time.now.utc.strftime("%Y-%d-%m")]
|
22
|
-
)
|
23
|
-
RateResponse.new(true, "Success (delayed #{delay_time} seconds)", {:rate => 'free'}, :rates => [bogus_estimate], :xml => "<rate>free</rate>")
|
24
|
-
end
|
25
|
-
|
26
|
-
private
|
27
|
-
|
28
|
-
def generate_simulated_lag(max_delay = 30)
|
29
|
-
sleep Random.rand * max_delay
|
30
|
-
end
|
31
|
-
end
|
32
|
-
end
|
33
|
-
end
|
@@ -1,14 +0,0 @@
|
|
1
|
-
module ActiveMerchant
|
2
|
-
module Shipping
|
3
|
-
class BogusCarrier < Carrier
|
4
|
-
cattr_reader :name
|
5
|
-
@@name = "Bogus Carrier"
|
6
|
-
|
7
|
-
def find_rates(origin, destination, packages, options = {})
|
8
|
-
origin = Location.from(origin)
|
9
|
-
destination = Location.from(destination)
|
10
|
-
packages = Array(packages)
|
11
|
-
end
|
12
|
-
end
|
13
|
-
end
|
14
|
-
end
|
@@ -1,257 +0,0 @@
|
|
1
|
-
require 'cgi'
|
2
|
-
|
3
|
-
module ActiveMerchant
|
4
|
-
module Shipping
|
5
|
-
class CanadaPost < Carrier
|
6
|
-
# NOTE!
|
7
|
-
# A Merchant CPC Id must be assigned to you by Canada Post
|
8
|
-
# CPC_DEMO_XML is just a public domain account for testing
|
9
|
-
|
10
|
-
class CanadaPostRateResponse < RateResponse
|
11
|
-
attr_reader :boxes, :postal_outlets
|
12
|
-
|
13
|
-
def initialize(success, message, params = {}, options = {})
|
14
|
-
@boxes = options[:boxes]
|
15
|
-
@postal_outlets = options[:postal_outlets]
|
16
|
-
super
|
17
|
-
end
|
18
|
-
end
|
19
|
-
|
20
|
-
cattr_reader :name, :name_french
|
21
|
-
@@name = "Canada Post"
|
22
|
-
@@name_french = "Postes Canada"
|
23
|
-
|
24
|
-
Box = Struct.new(:name, :weight, :expediter_weight, :length, :width, :height, :packedItems)
|
25
|
-
PackedItem = Struct.new(:quantity, :description)
|
26
|
-
PostalOutlet = Struct.new(:sequence_no, :distance, :name, :business_name, :postal_address, :business_hours)
|
27
|
-
|
28
|
-
URL = "http://sellonline.canadapost.ca:30000"
|
29
|
-
DOCTYPE = '<!DOCTYPE eparcel SYSTEM "http://sellonline.canadapost.ca/DevelopersResources/protocolV3/eParcel.dtd">'
|
30
|
-
|
31
|
-
RESPONSE_CODES = {
|
32
|
-
'1' => "All calculation was done",
|
33
|
-
'2' => "Default shipping rates are returned due to a problem during the processing of the request.",
|
34
|
-
'-2' => "Missing argument when calling module",
|
35
|
-
'-5' => "No Item to ship",
|
36
|
-
'-6' => "Illegal Item weight",
|
37
|
-
'-7' => "Illegal item dimension",
|
38
|
-
'-12' => "Can't open IM config file",
|
39
|
-
'-13' => "Can't create log files",
|
40
|
-
'-15' => "Invalid config file format",
|
41
|
-
'-102' => "Invalid socket connection",
|
42
|
-
'-106' => "Can't connect to server",
|
43
|
-
'-1000' => "Unknow request type sent by client",
|
44
|
-
'-1002' => "MAS Timed out",
|
45
|
-
'-1004' => "Socket communication break",
|
46
|
-
'-1005' => "Did not receive required data on socket.",
|
47
|
-
'-2000' => "Unable to estabish socket connection with RSSS",
|
48
|
-
'-2001' => "Merchant Id not found on server",
|
49
|
-
'-2002' => "One or more parameter was not sent by the IM to the MAS",
|
50
|
-
'-2003' => "Did not receive required data on socket.",
|
51
|
-
'-2004' => "The request contains to many items to process it.",
|
52
|
-
'-2005' => "The request received on socket is larger than the maximum allowed.",
|
53
|
-
'-3000' => "Origin Postal Code is illegal",
|
54
|
-
'-3001' => "Destination Postal Code/State Name/ Country is illegal",
|
55
|
-
'-3002' => "Parcel too large to be shipped with CPC",
|
56
|
-
'-3003' => "Parcel too small to be shipped with CPC",
|
57
|
-
'-3004' => "Parcel too heavy to be shipped with CPC",
|
58
|
-
'-3005' => "Internal error code returned by the rating DLL",
|
59
|
-
'-3006' => "The pick up time format is invalid or not defined.",
|
60
|
-
'-4000' => "Volumetric internal error",
|
61
|
-
'-4001' => "Volumetric time out calculation error.",
|
62
|
-
'-4002' => "No bins provided to the volumetric engine.",
|
63
|
-
'-4003' => "No items provided to the volumetric engine.",
|
64
|
-
'-4004' => "Item is too large to be packed",
|
65
|
-
'-4005' => "Number of item more than maximum allowed",
|
66
|
-
'-5000' => "XML Parsing error",
|
67
|
-
'-5001' => "XML Tag not found",
|
68
|
-
'-5002' => "Node Value Number format error",
|
69
|
-
'-5003' => "Node value is empty",
|
70
|
-
'-5004' => "Unable to create/parse XML Document",
|
71
|
-
'-6000' => "Unable to open the database",
|
72
|
-
'-6001' => "Unable to read from the database",
|
73
|
-
'-6002' => "Unable to write to the database",
|
74
|
-
'-50000' => "Internal problem - Please contact Sell Online Help Desk"
|
75
|
-
}
|
76
|
-
|
77
|
-
NON_ISO_COUNTRY_NAMES = {
|
78
|
-
'Russian Federation' => 'Russia'
|
79
|
-
}
|
80
|
-
|
81
|
-
def requirements
|
82
|
-
[:login]
|
83
|
-
end
|
84
|
-
|
85
|
-
def find_rates(origin, destination, line_items = [], options = {})
|
86
|
-
rate_request = build_rate_request(origin, destination, line_items, options)
|
87
|
-
commit(rate_request, origin, destination, options)
|
88
|
-
end
|
89
|
-
|
90
|
-
def maximum_weight
|
91
|
-
Mass.new(30, :kilograms)
|
92
|
-
end
|
93
|
-
|
94
|
-
def self.default_location
|
95
|
-
{
|
96
|
-
:country => 'CA',
|
97
|
-
:province => 'ON',
|
98
|
-
:city => 'Ottawa',
|
99
|
-
:address1 => '61A York St',
|
100
|
-
:postal_code => 'K1N5T2'
|
101
|
-
}
|
102
|
-
end
|
103
|
-
|
104
|
-
protected
|
105
|
-
|
106
|
-
def commit(request, origin, destination, options = {})
|
107
|
-
parse_rate_response(ssl_post(URL, request), origin, destination, options)
|
108
|
-
end
|
109
|
-
|
110
|
-
private
|
111
|
-
|
112
|
-
def build_rate_request(origin, destination, line_items = [], options = {})
|
113
|
-
line_items = [line_items] unless line_items.is_a?(Array)
|
114
|
-
origin = origin.is_a?(Location) ? origin : Location.new(origin)
|
115
|
-
destination = destination.is_a?(Location) ? destination : Location.new(destination)
|
116
|
-
|
117
|
-
xml_request = XmlNode.new('eparcel') do |root_node|
|
118
|
-
root_node << XmlNode.new('language', @options[:french] ? 'fr' : 'en')
|
119
|
-
root_node << XmlNode.new('ratesAndServicesRequest') do |request|
|
120
|
-
|
121
|
-
request << XmlNode.new('merchantCPCID', @options[:login])
|
122
|
-
request << XmlNode.new('fromPostalCode', origin.postal_code)
|
123
|
-
request << XmlNode.new('turnAroundTime', options[:turn_around_time]) if options[:turn_around_time]
|
124
|
-
request << XmlNode.new('itemsPrice', dollar_amount(line_items.map(&:value).compact.sum))
|
125
|
-
|
126
|
-
# line items
|
127
|
-
request << build_line_items(line_items)
|
128
|
-
|
129
|
-
# delivery info
|
130
|
-
# NOTE: These tags MUST be after line items
|
131
|
-
request << XmlNode.new('city', destination.city)
|
132
|
-
request << XmlNode.new('provOrState', destination.province)
|
133
|
-
request << XmlNode.new('country', handle_non_iso_country_names(destination.country))
|
134
|
-
request << XmlNode.new('postalCode', destination.postal_code)
|
135
|
-
end
|
136
|
-
end
|
137
|
-
|
138
|
-
DOCTYPE + xml_request.to_s
|
139
|
-
end
|
140
|
-
|
141
|
-
def parse_rate_response(response, origin, destination, options = {})
|
142
|
-
xml = REXML::Document.new(response)
|
143
|
-
success = response_success?(xml)
|
144
|
-
message = response_message(xml)
|
145
|
-
|
146
|
-
rate_estimates = []
|
147
|
-
boxes = []
|
148
|
-
if success
|
149
|
-
xml.elements.each('eparcel/ratesAndServicesResponse/product') do |product|
|
150
|
-
service_name = (@options[:french] ? @@name_french : @@name) + " " + product.get_text('name').to_s
|
151
|
-
service_code = product.attribute('id').to_s
|
152
|
-
|
153
|
-
rate_estimates << RateEstimate.new(origin, destination, @@name, service_name,
|
154
|
-
:service_code => service_code,
|
155
|
-
:total_price => product.get_text('rate').to_s,
|
156
|
-
:currency => 'CAD',
|
157
|
-
:shipping_date => product.get_text('shippingDate').to_s,
|
158
|
-
:delivery_range => [product.get_text('deliveryDate').to_s] * 2
|
159
|
-
)
|
160
|
-
end
|
161
|
-
|
162
|
-
boxes = xml.elements.collect('eparcel/ratesAndServicesResponse/packing/box') do |box|
|
163
|
-
b = Box.new
|
164
|
-
b.packedItems = []
|
165
|
-
b.name = box.get_text('name').to_s
|
166
|
-
b.weight = box.get_text('weight').to_s.to_f
|
167
|
-
b.expediter_weight = box.get_text('expediterWeight').to_s.to_f
|
168
|
-
b.length = box.get_text('length').to_s.to_f
|
169
|
-
b.width = box.get_text('width').to_s.to_f
|
170
|
-
b.height = box.get_text('height').to_s.to_f
|
171
|
-
b.packedItems = box.elements.collect('packedItem') do |item|
|
172
|
-
p = PackedItem.new
|
173
|
-
p.quantity = item.get_text('quantity').to_s.to_i
|
174
|
-
p.description = item.get_text('description').to_s
|
175
|
-
p
|
176
|
-
end
|
177
|
-
b
|
178
|
-
end
|
179
|
-
|
180
|
-
postal_outlets = xml.elements.collect('eparcel/ratesAndServicesResponse/nearestPostalOutlet') do |outlet|
|
181
|
-
postal_outlet = PostalOutlet.new
|
182
|
-
postal_outlet.sequence_no = outlet.get_text('postalOutletSequenceNo').to_s
|
183
|
-
postal_outlet.distance = outlet.get_text('distance').to_s
|
184
|
-
postal_outlet.name = outlet.get_text('outletName').to_s
|
185
|
-
postal_outlet.business_name = outlet.get_text('businessName').to_s
|
186
|
-
|
187
|
-
postal_outlet.postal_address = Location.new(
|
188
|
-
:address1 => outlet.get_text('postalAddress/addressLine').to_s,
|
189
|
-
:postal_code => outlet.get_text('postalAddress/postal_code').to_s,
|
190
|
-
:city => outlet.get_text('postalAddress/municipality').to_s,
|
191
|
-
:province => outlet.get_text('postalAddress/province').to_s,
|
192
|
-
:country => 'Canada',
|
193
|
-
:phone_number => outlet.get_text('phoneNumber').to_s
|
194
|
-
)
|
195
|
-
|
196
|
-
postal_outlet.business_hours = outlet.elements.collect('businessHours') do |hour|
|
197
|
-
{ :day_of_week => hour.get_text('dayOfWeek').to_s, :time => hour.get_text('time').to_s }
|
198
|
-
end
|
199
|
-
|
200
|
-
postal_outlet
|
201
|
-
end
|
202
|
-
end
|
203
|
-
|
204
|
-
CanadaPostRateResponse.new(success, message, Hash.from_xml(response), :rates => rate_estimates, :xml => response, :boxes => boxes, :postal_outlets => postal_outlets)
|
205
|
-
end
|
206
|
-
|
207
|
-
def response_success?(xml)
|
208
|
-
return false unless xml.get_text('eparcel/error').nil?
|
209
|
-
|
210
|
-
value = xml.get_text('eparcel/ratesAndServicesResponse/statusCode').to_s
|
211
|
-
value == '1' || value == '2'
|
212
|
-
end
|
213
|
-
|
214
|
-
def response_message(xml)
|
215
|
-
if response_success?(xml)
|
216
|
-
xml.get_text('eparcel/ratesAndServicesResponse/statusMessage').to_s
|
217
|
-
else
|
218
|
-
xml.get_text('eparcel/error/statusMessage').to_s
|
219
|
-
end
|
220
|
-
end
|
221
|
-
|
222
|
-
# <!-- List of items in the shopping -->
|
223
|
-
# <!-- cart -->
|
224
|
-
# <!-- Each item is defined by : -->
|
225
|
-
# <!-- - quantity (mandatory) -->
|
226
|
-
# <!-- - size (mandatory) -->
|
227
|
-
# <!-- - weight (mandatory) -->
|
228
|
-
# <!-- - description (mandatory) -->
|
229
|
-
# <!-- - ready to ship (optional) -->
|
230
|
-
|
231
|
-
def build_line_items(line_items)
|
232
|
-
XmlNode.new('lineItems') do |line_items_node|
|
233
|
-
line_items.each do |line_item|
|
234
|
-
line_items_node << XmlNode.new('item') do |item|
|
235
|
-
item << XmlNode.new('quantity', 1)
|
236
|
-
item << XmlNode.new('weight', line_item.kilograms)
|
237
|
-
item << XmlNode.new('length', line_item.cm(:length).to_s)
|
238
|
-
item << XmlNode.new('width', line_item.cm(:width).to_s)
|
239
|
-
item << XmlNode.new('height', line_item.cm(:height).to_s)
|
240
|
-
item << XmlNode.new('description', line_item.options[:description] || ' ')
|
241
|
-
item << XmlNode.new('readyToShip', line_item.options[:ready_to_ship] || nil)
|
242
|
-
# By setting the 'readyToShip' tag to true, Sell Online will not pack this item in the boxes defined in the merchant profile.
|
243
|
-
end
|
244
|
-
end
|
245
|
-
end
|
246
|
-
end
|
247
|
-
|
248
|
-
def dollar_amount(cents)
|
249
|
-
"%0.2f" % (cents / 100.0)
|
250
|
-
end
|
251
|
-
|
252
|
-
def handle_non_iso_country_names(country)
|
253
|
-
NON_ISO_COUNTRY_NAMES[country.to_s] || country
|
254
|
-
end
|
255
|
-
end
|
256
|
-
end
|
257
|
-
end
|
@@ -1,874 +0,0 @@
|
|
1
|
-
require 'cgi'
|
2
|
-
|
3
|
-
module ActiveMerchant
|
4
|
-
module Shipping
|
5
|
-
class CanadaPostPWS < Carrier
|
6
|
-
@@name = "Canada Post PWS"
|
7
|
-
|
8
|
-
SHIPPING_SERVICES = {
|
9
|
-
"DOM.RP" => "Regular Parcel",
|
10
|
-
"DOM.EP" => "Expedited Parcel",
|
11
|
-
"DOM.XP" => "Xpresspost",
|
12
|
-
"DOM.XP.CERT" => "Xpresspost Certified",
|
13
|
-
"DOM.PC" => "Priority",
|
14
|
-
"DOM.LIB" => "Library Books",
|
15
|
-
|
16
|
-
"USA.EP" => "Expedited Parcel USA",
|
17
|
-
"USA.PW.ENV" => "Priority Worldwide Envelope USA",
|
18
|
-
"USA.PW.PAK" => "Priority Worldwide pak USA",
|
19
|
-
"USA.PW.PARCEL" => "Priority Worldwide Parcel USA",
|
20
|
-
"USA.SP.AIR" => "Small Packet USA Air",
|
21
|
-
"USA.SP.SURF" => "Small Packet USA Surface",
|
22
|
-
"USA.XP" => "Xpresspost USA",
|
23
|
-
|
24
|
-
"INT.XP" => "Xpresspost International",
|
25
|
-
"INT.IP.AIR" => "International Parcel Air",
|
26
|
-
"INT.IP.SURF" => "International Parcel Surface",
|
27
|
-
"INT.PW.ENV" => "Priority Worldwide Envelope Int'l",
|
28
|
-
"INT.PW.PAK" => "Priority Worldwide pak Int'l",
|
29
|
-
"INT.PW.PARCEL" => "Priority Worldwide parcel Int'l",
|
30
|
-
"INT.SP.AIR" => "Small Packet International Air",
|
31
|
-
"INT.SP.SURF" => "Small Packet International Surface"
|
32
|
-
}
|
33
|
-
|
34
|
-
ENDPOINT = "https://soa-gw.canadapost.ca/" # production
|
35
|
-
|
36
|
-
SHIPMENT_MIMETYPE = "application/vnd.cpc.ncshipment+xml"
|
37
|
-
RATE_MIMETYPE = "application/vnd.cpc.ship.rate+xml"
|
38
|
-
TRACK_MIMETYPE = "application/vnd.cpc.track+xml"
|
39
|
-
REGISTER_MIMETYPE = "application/vnd.cpc.registration+xml"
|
40
|
-
|
41
|
-
LANGUAGE = {
|
42
|
-
'en' => 'en-CA',
|
43
|
-
'fr' => 'fr-CA'
|
44
|
-
}
|
45
|
-
|
46
|
-
SHIPPING_OPTIONS = [:d2po, :d2po_office_id, :cov, :cov_amount, :cod, :cod_amount, :cod_includes_shipping,
|
47
|
-
:cod_method_of_payment, :so, :dc, :dns, :pa18, :pa19, :hfp, :lad,
|
48
|
-
:rase, :rts, :aban]
|
49
|
-
|
50
|
-
RATES_OPTIONS = [:cov, :cov_amount, :cod, :so, :dc, :dns, :pa18, :pa19, :hfp, :lad]
|
51
|
-
|
52
|
-
MAX_WEIGHT = 30 # kg
|
53
|
-
|
54
|
-
attr_accessor :language, :endpoint, :logger, :platform_id, :customer_number
|
55
|
-
|
56
|
-
def initialize(options = {})
|
57
|
-
@language = LANGUAGE[options[:language]] || LANGUAGE['en']
|
58
|
-
@endpoint = options[:endpoint] || ENDPOINT
|
59
|
-
@platform_id = options[:platform_id]
|
60
|
-
@customer_number = options[:customer_number]
|
61
|
-
super(options)
|
62
|
-
end
|
63
|
-
|
64
|
-
def requirements
|
65
|
-
[:api_key, :secret]
|
66
|
-
end
|
67
|
-
|
68
|
-
def find_rates(origin, destination, line_items = [], options = {}, package = nil, services = [])
|
69
|
-
url = endpoint + "rs/ship/price"
|
70
|
-
request = build_rates_request(origin, destination, line_items, options, package, services)
|
71
|
-
response = ssl_post(url, request, headers(options, RATE_MIMETYPE, RATE_MIMETYPE))
|
72
|
-
parse_rates_response(response, origin, destination)
|
73
|
-
rescue ActiveMerchant::ResponseError, ActiveMerchant::Shipping::ResponseError => e
|
74
|
-
error_response(e.response.body, CPPWSRateResponse)
|
75
|
-
end
|
76
|
-
|
77
|
-
def find_tracking_info(pin, options = {})
|
78
|
-
response = ssl_get(tracking_url(pin), headers(options, TRACK_MIMETYPE))
|
79
|
-
parse_tracking_response(response)
|
80
|
-
rescue ActiveMerchant::ResponseError, ActiveMerchant::Shipping::ResponseError => e
|
81
|
-
if e.response
|
82
|
-
error_response(e.response.body, CPPWSTrackingResponse)
|
83
|
-
else
|
84
|
-
CPPWSTrackingResponse.new(false, e.message, {}, :carrier => @@name)
|
85
|
-
end
|
86
|
-
rescue InvalidPinFormatError
|
87
|
-
CPPWSTrackingResponse.new(false, "Invalid Pin Format", {}, :carrier => @@name)
|
88
|
-
end
|
89
|
-
|
90
|
-
# line_items should be a list of PackageItem's
|
91
|
-
def create_shipment(origin, destination, package, line_items = [], options = {})
|
92
|
-
request_body = build_shipment_request(origin, destination, package, line_items, options)
|
93
|
-
response = ssl_post(create_shipment_url(options), request_body, headers(options, SHIPMENT_MIMETYPE, SHIPMENT_MIMETYPE))
|
94
|
-
parse_shipment_response(response)
|
95
|
-
rescue ActiveMerchant::ResponseError, ActiveMerchant::Shipping::ResponseError => e
|
96
|
-
error_response(e.response.body, CPPWSShippingResponse)
|
97
|
-
rescue MissingCustomerNumberError
|
98
|
-
CPPWSShippingResponse.new(false, "Missing Customer Number", {}, :carrier => @@name)
|
99
|
-
end
|
100
|
-
|
101
|
-
def retrieve_shipment(shipping_id, options = {})
|
102
|
-
response = ssl_post(shipment_url(shipping_id, options), nil, headers(options, SHIPMENT_MIMETYPE, SHIPMENT_MIMETYPE))
|
103
|
-
parse_shipment_response(response)
|
104
|
-
end
|
105
|
-
|
106
|
-
def find_shipment_receipt(shipping_id, options = {})
|
107
|
-
response = ssl_get(shipment_receipt_url(shipping_id, options), headers(options, SHIPMENT_MIMETYPE, SHIPMENT_MIMETYPE))
|
108
|
-
parse_shipment_receipt_response(response)
|
109
|
-
end
|
110
|
-
|
111
|
-
def retrieve_shipping_label(shipping_response, options = {})
|
112
|
-
raise MissingShippingNumberError unless shipping_response && shipping_response.shipping_id
|
113
|
-
ssl_get(shipping_response.label_url, headers(options, "application/pdf"))
|
114
|
-
end
|
115
|
-
|
116
|
-
def register_merchant(options = {})
|
117
|
-
url = endpoint + "ot/token"
|
118
|
-
response = ssl_post(url, nil, headers({}, REGISTER_MIMETYPE, REGISTER_MIMETYPE).merge("Content-Length" => "0"))
|
119
|
-
parse_register_token_response(response)
|
120
|
-
rescue ActiveMerchant::ResponseError, ActiveMerchant::Shipping::ResponseError => e
|
121
|
-
error_response(e.response.body, CPPWSRegisterResponse)
|
122
|
-
end
|
123
|
-
|
124
|
-
def retrieve_merchant_details(options = {})
|
125
|
-
raise MissingTokenIdError unless token_id = options[:token_id]
|
126
|
-
url = endpoint + "ot/token/#{token_id}"
|
127
|
-
response = ssl_get(url, headers({}, REGISTER_MIMETYPE, REGISTER_MIMETYPE))
|
128
|
-
parse_merchant_details_response(response)
|
129
|
-
rescue ActiveMerchant::ResponseError, ActiveMerchant::Shipping::ResponseError => e
|
130
|
-
error_response(e.response.body, CPPWSMerchantDetailsResponse)
|
131
|
-
rescue Exception => e
|
132
|
-
raise ResponseError.new(e.message)
|
133
|
-
end
|
134
|
-
|
135
|
-
def find_services(country = nil, options = {})
|
136
|
-
response = ssl_get(services_url(country), headers(options, RATE_MIMETYPE))
|
137
|
-
parse_services_response(response)
|
138
|
-
rescue ActiveMerchant::ResponseError, ActiveMerchant::Shipping::ResponseError => e
|
139
|
-
error_response(e.response.body, CPPWSRateResponse)
|
140
|
-
end
|
141
|
-
|
142
|
-
def find_service_options(service_code, country, options = {})
|
143
|
-
response = ssl_get(services_url(country, service_code), headers(options, RATE_MIMETYPE))
|
144
|
-
parse_service_options_response(response)
|
145
|
-
rescue ActiveMerchant::ResponseError, ActiveMerchant::Shipping::ResponseError => e
|
146
|
-
error_response(e.response.body, CPPWSRateResponse)
|
147
|
-
end
|
148
|
-
|
149
|
-
def find_option_details(option_code, options = {})
|
150
|
-
url = endpoint + "rs/ship/option/#{option_code}"
|
151
|
-
response = ssl_get(url, headers(options, RATE_MIMETYPE))
|
152
|
-
parse_option_response(response)
|
153
|
-
rescue ActiveMerchant::ResponseError, ActiveMerchant::Shipping::ResponseError => e
|
154
|
-
error_response(e.response.body, CPPWSRateResponse)
|
155
|
-
end
|
156
|
-
|
157
|
-
def maximum_weight
|
158
|
-
Mass.new(MAX_WEIGHT, :kilograms)
|
159
|
-
end
|
160
|
-
|
161
|
-
# service discovery
|
162
|
-
|
163
|
-
def parse_services_response(response)
|
164
|
-
doc = REXML::Document.new(REXML::Text::unnormalize(response))
|
165
|
-
service_nodes = doc.elements['services'].elements.collect('service') { |node| node }
|
166
|
-
service_nodes.inject({}) do |result, node|
|
167
|
-
service_code = node.get_text("service-code").to_s
|
168
|
-
service_name = node.get_text("service-name").to_s
|
169
|
-
service_link = node.elements["link"].attributes['href']
|
170
|
-
service_link_media_type = node.elements["link"].attributes['media-type']
|
171
|
-
result[service_code] = {
|
172
|
-
:name => service_name,
|
173
|
-
:link => service_link,
|
174
|
-
:link_media_type => service_link_media_type
|
175
|
-
}
|
176
|
-
result
|
177
|
-
end
|
178
|
-
end
|
179
|
-
|
180
|
-
def parse_service_options_response(response)
|
181
|
-
doc = REXML::Document.new(REXML::Text::unnormalize(response))
|
182
|
-
service_node = doc.elements['service']
|
183
|
-
service_code = service_node.get_text("service-code").to_s
|
184
|
-
service_name = service_node.get_text("service-name").to_s
|
185
|
-
options_node = service_node.elements['options']
|
186
|
-
unless options_node.blank?
|
187
|
-
option_nodes = options_node.elements.collect('option') { |node| node }
|
188
|
-
options = option_nodes.inject([]) do |result, node|
|
189
|
-
option = {
|
190
|
-
:code => node.get_text("option-code").to_s,
|
191
|
-
:name => node.get_text("option-name").to_s,
|
192
|
-
:required => node.get_text("mandatory").to_s == "false" ? false : true,
|
193
|
-
:qualifier_required => node.get_text("qualifier-required").to_s == "false" ? false : true
|
194
|
-
}
|
195
|
-
option[:qualifier_max] = node.get_text("qualifier-max").to_s.to_i if node.get_text("qualifier-max")
|
196
|
-
result << option
|
197
|
-
result
|
198
|
-
end
|
199
|
-
end
|
200
|
-
restrictions_node = service_node.elements['restrictions']
|
201
|
-
dimensions_node = restrictions_node.elements['dimensional-restrictions']
|
202
|
-
restrictions = {
|
203
|
-
:min_weight => restrictions_node.elements["weight-restriction"].attributes['min'].to_i,
|
204
|
-
:max_weight => restrictions_node.elements["weight-restriction"].attributes['max'].to_i,
|
205
|
-
:min_length => dimensions_node.elements["length"].attributes['min'].to_f,
|
206
|
-
:max_length => dimensions_node.elements["length"].attributes['max'].to_f,
|
207
|
-
:min_height => dimensions_node.elements["height"].attributes['min'].to_f,
|
208
|
-
:max_height => dimensions_node.elements["height"].attributes['max'].to_f,
|
209
|
-
:min_width => dimensions_node.elements["width"].attributes['min'].to_f,
|
210
|
-
:max_width => dimensions_node.elements["width"].attributes['max'].to_f
|
211
|
-
}
|
212
|
-
|
213
|
-
{
|
214
|
-
:service_code => service_code,
|
215
|
-
:service_name => service_name,
|
216
|
-
:options => options,
|
217
|
-
:restrictions => restrictions
|
218
|
-
}
|
219
|
-
end
|
220
|
-
|
221
|
-
def parse_option_response(response)
|
222
|
-
doc = REXML::Document.new(REXML::Text::unnormalize(response))
|
223
|
-
option_node = doc.elements['option']
|
224
|
-
conflicts = option_node.elements['conflicting-options'].elements.collect('option-code') { |node| node.get_text.to_s } unless option_node.elements['conflicting-options'].blank?
|
225
|
-
prereqs = option_node.elements['prerequisite-options'].elements.collect('option-code') { |node| node.get_text.to_s } unless option_node.elements['prerequisite-options'].blank?
|
226
|
-
option = {
|
227
|
-
:code => option_node.get_text('option-code').to_s,
|
228
|
-
:name => option_node.get_text('option-name').to_s,
|
229
|
-
:class => option_node.get_text('option-class').to_s,
|
230
|
-
:prints_on_label => option_node.get_text('prints-on-label').to_s == "false" ? false : true,
|
231
|
-
:qualifier_required => option_node.get_text('qualifier-required').to_s == "false" ? false : true
|
232
|
-
}
|
233
|
-
option[:conflicting_options] = conflicts if conflicts
|
234
|
-
option[:prerequisite_options] = prereqs if prereqs
|
235
|
-
|
236
|
-
option[:qualifier_max] = option_node.get_text("qualifier-max").to_s.to_i if option_node.get_text("qualifier-max")
|
237
|
-
option
|
238
|
-
end
|
239
|
-
|
240
|
-
# rating
|
241
|
-
|
242
|
-
def build_rates_request(origin, destination, line_items = [], options = {}, package = nil, services = [])
|
243
|
-
line_items = Array(line_items)
|
244
|
-
xml = XmlNode.new('mailing-scenario', :xmlns => "http://www.canadapost.ca/ws/ship/rate") do |node|
|
245
|
-
node << customer_number_node(options)
|
246
|
-
node << contract_id_node(options)
|
247
|
-
node << quote_type_node(options)
|
248
|
-
node << expected_mailing_date_node(shipping_date(options)) if options[:shipping_date]
|
249
|
-
options_node = shipping_options_node(RATES_OPTIONS, options)
|
250
|
-
node << options_node if options_node && !options_node.children.count.zero?
|
251
|
-
node << parcel_node(line_items, package)
|
252
|
-
node << origin_node(origin)
|
253
|
-
node << destination_node(destination)
|
254
|
-
node << services_node(services) unless services.blank?
|
255
|
-
end
|
256
|
-
xml.to_s
|
257
|
-
end
|
258
|
-
|
259
|
-
def parse_rates_response(response, origin, destination)
|
260
|
-
doc = REXML::Document.new(REXML::Text::unnormalize(response))
|
261
|
-
raise ActiveMerchant::Shipping::ResponseError, "No Quotes" unless doc.elements['price-quotes']
|
262
|
-
|
263
|
-
quotes = doc.elements['price-quotes'].elements.collect('price-quote') { |node| node }
|
264
|
-
rates = quotes.map do |node|
|
265
|
-
service_name = node.get_text("service-name").to_s
|
266
|
-
service_code = node.get_text("service-code").to_s
|
267
|
-
total_price = node.elements['price-details'].get_text("due").to_s
|
268
|
-
expected_date = expected_date_from_node(node)
|
269
|
-
options = {
|
270
|
-
:service_code => service_code,
|
271
|
-
:total_price => total_price,
|
272
|
-
:currency => 'CAD',
|
273
|
-
:delivery_range => [expected_date, expected_date]
|
274
|
-
}
|
275
|
-
RateEstimate.new(origin, destination, @@name, service_name, options)
|
276
|
-
end
|
277
|
-
CPPWSRateResponse.new(true, "", {}, :rates => rates)
|
278
|
-
end
|
279
|
-
|
280
|
-
# tracking
|
281
|
-
|
282
|
-
def parse_tracking_response(response)
|
283
|
-
doc = REXML::Document.new(REXML::Text::unnormalize(response))
|
284
|
-
raise ActiveMerchant::Shipping::ResponseError, "No Tracking" unless root_node = doc.elements['tracking-detail']
|
285
|
-
|
286
|
-
events = root_node.elements['significant-events'].elements.collect('occurrence') { |node| node }
|
287
|
-
|
288
|
-
shipment_events = build_tracking_events(events)
|
289
|
-
change_date = root_node.get_text('changed-expected-date').to_s
|
290
|
-
expected_date = root_node.get_text('expected-delivery-date').to_s
|
291
|
-
dest_postal_code = root_node.get_text('destination-postal-id').to_s
|
292
|
-
destination = Location.new(:postal_code => dest_postal_code)
|
293
|
-
origin = Location.new(origin_hash_for(root_node))
|
294
|
-
options = {
|
295
|
-
:carrier => @@name,
|
296
|
-
:service_name => root_node.get_text('service-name').to_s,
|
297
|
-
:expected_date => expected_date.blank? ? nil : Date.parse(expected_date),
|
298
|
-
:changed_date => change_date.blank? ? nil : Date.parse(change_date),
|
299
|
-
:change_reason => root_node.get_text('changed-expected-delivery-reason').to_s.strip,
|
300
|
-
:destination_postal_code => root_node.get_text('destination-postal-id').to_s,
|
301
|
-
:shipment_events => shipment_events,
|
302
|
-
:tracking_number => root_node.get_text('pin').to_s,
|
303
|
-
:origin => origin,
|
304
|
-
:destination => destination,
|
305
|
-
:customer_number => root_node.get_text('mailed-by-customer-number').to_s
|
306
|
-
}
|
307
|
-
|
308
|
-
CPPWSTrackingResponse.new(true, "", {}, options)
|
309
|
-
end
|
310
|
-
|
311
|
-
def build_tracking_events(events)
|
312
|
-
events.map do |event|
|
313
|
-
date = event.get_text('event-date').to_s
|
314
|
-
time = event.get_text('event-time').to_s
|
315
|
-
zone = event.get_text('event-time-zone').to_s
|
316
|
-
timestamp = DateTime.parse("#{date} #{time} #{zone}")
|
317
|
-
time = Time.utc(timestamp.utc.year, timestamp.utc.month, timestamp.utc.day, timestamp.utc.hour, timestamp.utc.min, timestamp.utc.sec)
|
318
|
-
message = event.get_text('event-description').to_s
|
319
|
-
location = [event.get_text('event-retail-name'), event.get_text('event-site'), event.get_text('event-province')].compact.join(", ")
|
320
|
-
name = event.get_text('event-identifier').to_s
|
321
|
-
ShipmentEvent.new(name, time, location, message)
|
322
|
-
end
|
323
|
-
end
|
324
|
-
|
325
|
-
# shipping
|
326
|
-
|
327
|
-
# options
|
328
|
-
# :service => 'DOM.EP'
|
329
|
-
# :notification_email
|
330
|
-
# :packing_instructions
|
331
|
-
# :show_postage_rate
|
332
|
-
# :cod, :cod_amount, :insurance, :insurance_amount, :signature_required, :pa18, :pa19, :hfp, :dns, :lad
|
333
|
-
#
|
334
|
-
def build_shipment_request(origin, destination, package, line_items = [], options = {})
|
335
|
-
origin = sanitize_location(origin)
|
336
|
-
destination = sanitize_location(destination)
|
337
|
-
|
338
|
-
xml = XmlNode.new('non-contract-shipment', :xmlns => "http://www.canadapost.ca/ws/ncshipment") do |root_node|
|
339
|
-
root_node << XmlNode.new('delivery-spec') do |node|
|
340
|
-
node << shipment_service_code_node(options)
|
341
|
-
node << shipment_sender_node(origin, options)
|
342
|
-
node << shipment_destination_node(destination, options)
|
343
|
-
options_node = shipment_options_node(options)
|
344
|
-
node << shipment_options_node(options) if options_node && !options_node.children.count.zero?
|
345
|
-
node << shipment_parcel_node(package)
|
346
|
-
node << shipment_notification_node(options)
|
347
|
-
node << shipment_preferences_node(options)
|
348
|
-
node << references_node(options) # optional > user defined custom notes
|
349
|
-
node << shipment_customs_node(destination, line_items, options)
|
350
|
-
# COD Remittance defaults to sender
|
351
|
-
end
|
352
|
-
end
|
353
|
-
xml.to_s
|
354
|
-
end
|
355
|
-
|
356
|
-
def shipment_service_code_node(options)
|
357
|
-
XmlNode.new('service-code', options[:service])
|
358
|
-
end
|
359
|
-
|
360
|
-
def shipment_sender_node(location, options)
|
361
|
-
XmlNode.new('sender') do |node|
|
362
|
-
node << XmlNode.new('name', location.name)
|
363
|
-
node << XmlNode.new('company', location.company) if location.company.present?
|
364
|
-
node << XmlNode.new('contact-phone', location.phone)
|
365
|
-
node << XmlNode.new('address-details') do |innernode|
|
366
|
-
innernode << XmlNode.new('address-line-1', location.address1)
|
367
|
-
innernode << XmlNode.new('address-line-2', location.address2_and_3) unless location.address2_and_3.blank?
|
368
|
-
innernode << XmlNode.new('city', location.city)
|
369
|
-
innernode << XmlNode.new('prov-state', location.province)
|
370
|
-
# innernode << XmlNode.new('country-code', location.country_code)
|
371
|
-
innernode << XmlNode.new('postal-zip-code', location.postal_code)
|
372
|
-
end
|
373
|
-
end
|
374
|
-
end
|
375
|
-
|
376
|
-
def shipment_destination_node(location, options)
|
377
|
-
XmlNode.new('destination') do |node|
|
378
|
-
node << XmlNode.new('name', location.name)
|
379
|
-
node << XmlNode.new('company', location.company) if location.company.present?
|
380
|
-
node << XmlNode.new('client-voice-number', location.phone)
|
381
|
-
node << XmlNode.new('address-details') do |innernode|
|
382
|
-
innernode << XmlNode.new('address-line-1', location.address1)
|
383
|
-
innernode << XmlNode.new('address-line-2', location.address2_and_3) unless location.address2_and_3.blank?
|
384
|
-
innernode << XmlNode.new('city', location.city)
|
385
|
-
innernode << XmlNode.new('prov-state', location.province) unless location.province.blank?
|
386
|
-
innernode << XmlNode.new('country-code', location.country_code)
|
387
|
-
innernode << XmlNode.new('postal-zip-code', location.postal_code)
|
388
|
-
end
|
389
|
-
end
|
390
|
-
end
|
391
|
-
|
392
|
-
def shipment_options_node(options)
|
393
|
-
shipping_options_node(SHIPPING_OPTIONS, options)
|
394
|
-
end
|
395
|
-
|
396
|
-
def shipment_notification_node(options)
|
397
|
-
return unless options[:notification_email]
|
398
|
-
XmlNode.new('notification') do |node|
|
399
|
-
node << XmlNode.new('email', options[:notification_email])
|
400
|
-
node << XmlNode.new('on-shipment', true)
|
401
|
-
node << XmlNode.new('on-exception', true)
|
402
|
-
node << XmlNode.new('on-delivery', true)
|
403
|
-
end
|
404
|
-
end
|
405
|
-
|
406
|
-
def shipment_preferences_node(options)
|
407
|
-
XmlNode.new('preferences') do |node|
|
408
|
-
node << XmlNode.new('show-packing-instructions', options[:packing_instructions] || true)
|
409
|
-
node << XmlNode.new('show-postage-rate', options[:show_postage_rate] || false)
|
410
|
-
node << XmlNode.new('show-insured-value', true)
|
411
|
-
end
|
412
|
-
end
|
413
|
-
|
414
|
-
def references_node(options)
|
415
|
-
# custom values
|
416
|
-
# XmlNode.new('references') do |node|
|
417
|
-
# end
|
418
|
-
end
|
419
|
-
|
420
|
-
def shipment_customs_node(destination, line_items, options)
|
421
|
-
return unless destination.country_code != 'CA'
|
422
|
-
|
423
|
-
XmlNode.new('customs') do |node|
|
424
|
-
currency = options[:currency] || "CAD"
|
425
|
-
node << XmlNode.new('currency', currency)
|
426
|
-
node << XmlNode.new('conversion-from-cad', options[:conversion_from_cad].to_s) if currency != 'CAD' && options[:conversion_from_cad]
|
427
|
-
node << XmlNode.new('reason-for-export', 'SOG') # SOG - Sale of Goods
|
428
|
-
node << XmlNode.new('other-reason', options[:customs_other_reason]) if options[:customs_reason_for_export] && options[:customs_other_reason]
|
429
|
-
node << XmlNode.new('additional-customs-info', options[:customs_addition_info]) if options[:customs_addition_info]
|
430
|
-
node << XmlNode.new('sku-list') do |sku|
|
431
|
-
line_items.each do |line_item|
|
432
|
-
sku << XmlNode.new('item') do |item|
|
433
|
-
item << XmlNode.new('hs-tariff-code', line_item.hs_code) if line_item.hs_code && !line_item.hs_code.empty?
|
434
|
-
item << XmlNode.new('sku', line_item.sku) if line_item.sku && !line_item.sku.empty?
|
435
|
-
item << XmlNode.new('customs-description', line_item.name.slice(0, 44))
|
436
|
-
item << XmlNode.new('unit-weight', '%#2.3f' % sanitize_weight_kg(line_item.kg))
|
437
|
-
item << XmlNode.new('customs-value-per-unit', '%.2f' % sanitize_price_from_cents(line_item.value))
|
438
|
-
item << XmlNode.new('customs-number-of-units', line_item.quantity)
|
439
|
-
item << XmlNode.new('country-of-origin', line_item.options[:country_of_origin]) if line_item.options && line_item.options[:country_of_origin] && !line_item.options[:country_of_origin].empty?
|
440
|
-
item << XmlNode.new('province-of-origin', line_item.options[:province_of_origin]) if line_item.options && line_item.options[:province_of_origin] && !line_item.options[:province_of_origin].empty?
|
441
|
-
end
|
442
|
-
end
|
443
|
-
end
|
444
|
-
|
445
|
-
end
|
446
|
-
end
|
447
|
-
|
448
|
-
def shipment_parcel_node(package, options = {})
|
449
|
-
weight = sanitize_weight_kg(package.kilograms.to_f)
|
450
|
-
XmlNode.new('parcel-characteristics') do |el|
|
451
|
-
el << XmlNode.new('weight', "%#2.3f" % weight)
|
452
|
-
pkg_dim = package.cm
|
453
|
-
if pkg_dim && !pkg_dim.select { |x| x != 0 }.empty?
|
454
|
-
el << XmlNode.new('dimensions') do |dim|
|
455
|
-
dim << XmlNode.new('length', '%.1f' % ((pkg_dim[2] * 10).round / 10.0)) if pkg_dim.size >= 3
|
456
|
-
dim << XmlNode.new('width', '%.1f' % ((pkg_dim[1] * 10).round / 10.0)) if pkg_dim.size >= 2
|
457
|
-
dim << XmlNode.new('height', '%.1f' % ((pkg_dim[0] * 10).round / 10.0)) if pkg_dim.size >= 1
|
458
|
-
end
|
459
|
-
end
|
460
|
-
el << XmlNode.new('document', false)
|
461
|
-
el << XmlNode.new('mailing-tube', package.tube?)
|
462
|
-
el << XmlNode.new('unpackaged', package.unpackaged?)
|
463
|
-
end
|
464
|
-
end
|
465
|
-
|
466
|
-
def parse_shipment_response(response)
|
467
|
-
doc = REXML::Document.new(REXML::Text::unnormalize(response))
|
468
|
-
raise ActiveMerchant::Shipping::ResponseError, "No Shipping" unless root_node = doc.elements['non-contract-shipment-info']
|
469
|
-
options = {
|
470
|
-
:shipping_id => root_node.get_text('shipment-id').to_s,
|
471
|
-
:tracking_number => root_node.get_text('tracking-pin').to_s,
|
472
|
-
:details_url => root_node.elements["links/link[@rel='details']"].attributes['href'],
|
473
|
-
:label_url => root_node.elements["links/link[@rel='label']"].attributes['href'],
|
474
|
-
:receipt_url => root_node.elements["links/link[@rel='receipt']"].attributes['href']
|
475
|
-
}
|
476
|
-
CPPWSShippingResponse.new(true, "", {}, options)
|
477
|
-
end
|
478
|
-
|
479
|
-
def parse_register_token_response(response)
|
480
|
-
doc = REXML::Document.new(REXML::Text::unnormalize(response))
|
481
|
-
raise ActiveMerchant::Shipping::ResponseError, "No Registration Token" unless root_node = doc.elements['token']
|
482
|
-
options = {
|
483
|
-
:token_id => root_node.get_text('token-id').to_s
|
484
|
-
}
|
485
|
-
CPPWSRegisterResponse.new(true, "", {}, options)
|
486
|
-
end
|
487
|
-
|
488
|
-
def parse_merchant_details_response(response)
|
489
|
-
doc = REXML::Document.new(REXML::Text::unnormalize(response))
|
490
|
-
raise "No Merchant Info" unless root_node = doc.elements['merchant-info']
|
491
|
-
raise "No Merchant Info" if root_node.get_text('customer-number').blank?
|
492
|
-
options = {
|
493
|
-
:customer_number => root_node.get_text('customer-number').to_s,
|
494
|
-
:contract_number => root_node.get_text('contract-number').to_s,
|
495
|
-
:username => root_node.get_text('merchant-username').to_s,
|
496
|
-
:password => root_node.get_text('merchant-password').to_s,
|
497
|
-
:has_default_credit_card => root_node.get_text('has-default-credit-card') == 'true'
|
498
|
-
}
|
499
|
-
CPPWSMerchantDetailsResponse.new(true, "", {}, options)
|
500
|
-
end
|
501
|
-
|
502
|
-
def parse_shipment_receipt_response(response)
|
503
|
-
doc = REXML::Document.new(REXML::Text::unnormalize(response))
|
504
|
-
root = doc.elements['non-contract-shipment-receipt']
|
505
|
-
cc_details_node = root.elements['cc-receipt-details']
|
506
|
-
service_standard_node = root.elements['service-standard']
|
507
|
-
receipt = {
|
508
|
-
:final_shipping_point => root.get_text("final-shipping-point").to_s,
|
509
|
-
:shipping_point_name => root.get_text("shipping-point-name").to_s,
|
510
|
-
:service_code => root.get_text("service-code").to_s,
|
511
|
-
:rated_weight => root.get_text("rated-weight").to_s.to_f,
|
512
|
-
:base_amount => root.get_text("base-amount").to_s.to_f,
|
513
|
-
:pre_tax_amount => root.get_text("pre-tax-amount").to_s.to_f,
|
514
|
-
:gst_amount => root.get_text("gst-amount").to_s.to_f,
|
515
|
-
:pst_amount => root.get_text("pst-amount").to_s.to_f,
|
516
|
-
:hst_amount => root.get_text("hst-amount").to_s.to_f,
|
517
|
-
:charge_amount => cc_details_node.get_text("charge-amount").to_s.to_f,
|
518
|
-
:currency => cc_details_node.get_text("currency").to_s,
|
519
|
-
:expected_transit_days => service_standard_node.get_text("expected-transit-time").to_s.to_i,
|
520
|
-
:expected_delivery_date => service_standard_node.get_text("expected-delivery-date").to_s
|
521
|
-
}
|
522
|
-
option_nodes = root.elements['priced-options'].elements.collect('priced-option') { |node| node } unless root.elements['priced-options'].blank?
|
523
|
-
|
524
|
-
receipt[:priced_options] = if option_nodes
|
525
|
-
option_nodes.inject({}) do |result, node|
|
526
|
-
result[node.get_text("option-code").to_s] = node.get_text("option-price").to_s.to_f
|
527
|
-
result
|
528
|
-
end
|
529
|
-
else
|
530
|
-
[]
|
531
|
-
end
|
532
|
-
|
533
|
-
receipt
|
534
|
-
end
|
535
|
-
|
536
|
-
def error_response(response, response_klass)
|
537
|
-
doc = REXML::Document.new(REXML::Text::unnormalize(response))
|
538
|
-
messages = doc.elements['messages'].elements.collect('message') { |node| node }
|
539
|
-
message = messages.map { |m| m.get_text('description').to_s }.join(", ")
|
540
|
-
code = messages.map { |m| m.get_text('code').to_s }.join(", ")
|
541
|
-
response_klass.new(false, message, {}, :carrier => @@name, :code => code)
|
542
|
-
end
|
543
|
-
|
544
|
-
def log(msg)
|
545
|
-
logger.debug(msg) if logger
|
546
|
-
end
|
547
|
-
|
548
|
-
private
|
549
|
-
|
550
|
-
def tracking_url(pin)
|
551
|
-
case pin.length
|
552
|
-
when 12, 13, 16
|
553
|
-
endpoint + "vis/track/pin/%s/detail" % pin
|
554
|
-
when 15
|
555
|
-
endpoint + "vis/track/dnc/%s/detail" % pin
|
556
|
-
else
|
557
|
-
raise InvalidPinFormatError
|
558
|
-
end
|
559
|
-
end
|
560
|
-
|
561
|
-
def create_shipment_url(options)
|
562
|
-
raise MissingCustomerNumberError unless customer_number = options[:customer_number]
|
563
|
-
if @platform_id.present?
|
564
|
-
endpoint + "rs/#{customer_number}-#{@platform_id}/ncshipment"
|
565
|
-
else
|
566
|
-
endpoint + "rs/#{customer_number}/ncshipment"
|
567
|
-
end
|
568
|
-
end
|
569
|
-
|
570
|
-
def shipment_url(shipping_id, options = {})
|
571
|
-
raise MissingCustomerNumberError unless customer_number = options[:customer_number]
|
572
|
-
if @platform_id.present?
|
573
|
-
endpoint + "rs/#{customer_number}-#{@platform_id}/ncshipment/#{shipping_id}"
|
574
|
-
else
|
575
|
-
endpoint + "rs/#{customer_number}/ncshipment/#{shipping_id}"
|
576
|
-
end
|
577
|
-
end
|
578
|
-
|
579
|
-
def shipment_receipt_url(shipping_id, options = {})
|
580
|
-
raise MissingCustomerNumberError unless customer_number = options[:customer_number]
|
581
|
-
if @platform_id.present?
|
582
|
-
endpoint + "rs/#{customer_number}-#{@platform_id}/ncshipment/#{shipping_id}/receipt"
|
583
|
-
else
|
584
|
-
endpoint + "rs/#{customer_number}/ncshipment/#{shipping_id}/receipt"
|
585
|
-
end
|
586
|
-
end
|
587
|
-
|
588
|
-
def services_url(country = nil, service_code = nil)
|
589
|
-
url = endpoint + "rs/ship/service"
|
590
|
-
url += "/#{service_code}" if service_code
|
591
|
-
url += "?country=#{country}" if country
|
592
|
-
url
|
593
|
-
end
|
594
|
-
|
595
|
-
def customer_credentials_valid?(credentials)
|
596
|
-
(credentials.keys & [:customer_api_key, :customer_secret]).any?
|
597
|
-
end
|
598
|
-
|
599
|
-
def encoded_authorization(customer_credentials = {})
|
600
|
-
if customer_credentials_valid?(customer_credentials)
|
601
|
-
"Basic %s" % Base64.encode64("#{customer_credentials[:customer_api_key]}:#{customer_credentials[:customer_secret]}")
|
602
|
-
else
|
603
|
-
"Basic %s" % Base64.encode64("#{@options[:api_key]}:#{@options[:secret]}")
|
604
|
-
end
|
605
|
-
end
|
606
|
-
|
607
|
-
def headers(customer_credentials, accept = nil, content_type = nil)
|
608
|
-
headers = {
|
609
|
-
'Authorization' => encoded_authorization(customer_credentials),
|
610
|
-
'Accept-Language' => language
|
611
|
-
}
|
612
|
-
headers['Accept'] = accept if accept
|
613
|
-
headers['Content-Type'] = content_type if content_type
|
614
|
-
headers['Platform-ID'] = platform_id if platform_id && customer_credentials_valid?(customer_credentials)
|
615
|
-
headers
|
616
|
-
end
|
617
|
-
|
618
|
-
def customer_number_node(options)
|
619
|
-
XmlNode.new("customer-number", options[:customer_number] || customer_number)
|
620
|
-
end
|
621
|
-
|
622
|
-
def contract_id_node(options)
|
623
|
-
XmlNode.new("contract-id", options[:contract_id]) if options[:contract_id]
|
624
|
-
end
|
625
|
-
|
626
|
-
def quote_type_node(options)
|
627
|
-
XmlNode.new("quote-type", 'commercial')
|
628
|
-
end
|
629
|
-
|
630
|
-
def expected_mailing_date_node(date_as_string)
|
631
|
-
XmlNode.new("expected-mailing-date", date_as_string)
|
632
|
-
end
|
633
|
-
|
634
|
-
def parcel_node(line_items, package = nil, options = {})
|
635
|
-
weight = sanitize_weight_kg(package && !package.kilograms.zero? ? package.kilograms.to_f : line_items.sum(&:kilograms).to_f)
|
636
|
-
XmlNode.new('parcel-characteristics') do |el|
|
637
|
-
el << XmlNode.new('weight', "%#2.3f" % weight)
|
638
|
-
if package
|
639
|
-
pkg_dim = package.cm
|
640
|
-
if pkg_dim && !pkg_dim.select { |x| x != 0 }.empty?
|
641
|
-
el << XmlNode.new('dimensions') do |dim|
|
642
|
-
dim << XmlNode.new('length', '%.1f' % ((pkg_dim[2] * 10).round / 10.0)) if pkg_dim.size >= 3
|
643
|
-
dim << XmlNode.new('width', '%.1f' % ((pkg_dim[1] * 10).round / 10.0)) if pkg_dim.size >= 2
|
644
|
-
dim << XmlNode.new('height', '%.1f' % ((pkg_dim[0] * 10).round / 10.0)) if pkg_dim.size >= 1
|
645
|
-
end
|
646
|
-
end
|
647
|
-
end
|
648
|
-
el << XmlNode.new('mailing-tube', line_items.any?(&:tube?))
|
649
|
-
el << XmlNode.new('oversized', true) if line_items.any?(&:oversized?)
|
650
|
-
el << XmlNode.new('unpackaged', line_items.any?(&:unpackaged?))
|
651
|
-
end
|
652
|
-
end
|
653
|
-
|
654
|
-
def origin_node(location)
|
655
|
-
origin = sanitize_location(location)
|
656
|
-
XmlNode.new("origin-postal-code", origin.zip)
|
657
|
-
end
|
658
|
-
|
659
|
-
def destination_node(location)
|
660
|
-
destination = sanitize_location(location)
|
661
|
-
case destination.country_code
|
662
|
-
when 'CA'
|
663
|
-
XmlNode.new('destination') do |node|
|
664
|
-
node << XmlNode.new('domestic') do |x|
|
665
|
-
x << XmlNode.new('postal-code', destination.postal_code)
|
666
|
-
end
|
667
|
-
end
|
668
|
-
|
669
|
-
when 'US'
|
670
|
-
XmlNode.new('destination') do |node|
|
671
|
-
node << XmlNode.new('united-states') do |x|
|
672
|
-
x << XmlNode.new('zip-code', destination.postal_code)
|
673
|
-
end
|
674
|
-
end
|
675
|
-
|
676
|
-
else
|
677
|
-
XmlNode.new('destination') do |dest|
|
678
|
-
dest << XmlNode.new('international') do |dom|
|
679
|
-
dom << XmlNode.new('country-code', destination.country_code)
|
680
|
-
end
|
681
|
-
end
|
682
|
-
end
|
683
|
-
end
|
684
|
-
|
685
|
-
def services_node(services)
|
686
|
-
XmlNode.new('services') do |node|
|
687
|
-
services.each { |code| node << XmlNode.new('service-code', code) }
|
688
|
-
end
|
689
|
-
end
|
690
|
-
|
691
|
-
def shipping_options_node(available_options, options = {})
|
692
|
-
return if (options.symbolize_keys.keys & available_options).empty?
|
693
|
-
XmlNode.new('options') do |el|
|
694
|
-
|
695
|
-
if options[:cod] && options[:cod_amount]
|
696
|
-
el << XmlNode.new('option') do |opt|
|
697
|
-
opt << XmlNode.new('option-code', 'COD')
|
698
|
-
opt << XmlNode.new('option-amount', options[:cod_amount])
|
699
|
-
opt << XmlNode.new('option-qualifier-1', options[:cod_includes_shipping]) unless options[:cod_includes_shipping].blank?
|
700
|
-
opt << XmlNode.new('option-qualifier-2', options[:cod_method_of_payment]) unless options[:cod_method_of_payment].blank?
|
701
|
-
end
|
702
|
-
end
|
703
|
-
|
704
|
-
if options[:cov]
|
705
|
-
el << XmlNode.new('option') do |opt|
|
706
|
-
opt << XmlNode.new('option-code', 'COV')
|
707
|
-
opt << XmlNode.new('option-amount', options[:cov_amount]) unless options[:cov_amount].blank?
|
708
|
-
end
|
709
|
-
end
|
710
|
-
|
711
|
-
if options[:d2po]
|
712
|
-
el << XmlNode.new('option') do |opt|
|
713
|
-
opt << XmlNode.new('option-code', 'D2PO')
|
714
|
-
opt << XmlNode.new('option-qualifier-2'. options[:d2po_office_id]) unless options[:d2po_office_id].blank?
|
715
|
-
end
|
716
|
-
end
|
717
|
-
|
718
|
-
[:so, :dc, :pa18, :pa19, :hfp, :dns, :lad, :rase, :rts, :aban].each do |code|
|
719
|
-
if options[code]
|
720
|
-
el << XmlNode.new('option') do |opt|
|
721
|
-
opt << XmlNode.new('option-code', code.to_s.upcase)
|
722
|
-
end
|
723
|
-
end
|
724
|
-
end
|
725
|
-
end
|
726
|
-
end
|
727
|
-
|
728
|
-
def expected_date_from_node(node)
|
729
|
-
if service = node.elements['service-standard']
|
730
|
-
expected_date = service.get_text("expected-delivery-date").to_s
|
731
|
-
else
|
732
|
-
expected_date = nil
|
733
|
-
end
|
734
|
-
expected_date
|
735
|
-
end
|
736
|
-
|
737
|
-
def shipping_date(options)
|
738
|
-
DateTime.strptime((options[:shipping_date] || Time.now).to_s, "%Y-%m-%d")
|
739
|
-
end
|
740
|
-
|
741
|
-
def sanitize_location(location)
|
742
|
-
location_hash = location.is_a?(Location) ? location.to_hash : location
|
743
|
-
location_hash = sanitize_zip(location_hash)
|
744
|
-
Location.new(location_hash)
|
745
|
-
end
|
746
|
-
|
747
|
-
def sanitize_zip(hash)
|
748
|
-
[:postal_code, :zip].each do |attr|
|
749
|
-
hash[attr].gsub!(/\s+/, '') if hash[attr]
|
750
|
-
end
|
751
|
-
hash
|
752
|
-
end
|
753
|
-
|
754
|
-
def sanitize_weight_kg(kg)
|
755
|
-
kg == 0 ? 0.001 : kg
|
756
|
-
end
|
757
|
-
|
758
|
-
def sanitize_price_from_cents(value)
|
759
|
-
value == 0 ? 0.01 : value.round / 100.0
|
760
|
-
end
|
761
|
-
|
762
|
-
def origin_hash_for(root_node)
|
763
|
-
occurrences = root_node.get_elements('significant-events').first.get_elements('occurrence')
|
764
|
-
earliest = occurrences.sort_by { |occurrence| time_of_occurrence occurrence }.first
|
765
|
-
|
766
|
-
{
|
767
|
-
city: earliest.get_text('event-site').to_s,
|
768
|
-
province: earliest.get_text('event-province').to_s,
|
769
|
-
address_1: earliest.get_text('event-retail-location-id').to_s,
|
770
|
-
country: 'Canada'
|
771
|
-
}
|
772
|
-
end
|
773
|
-
|
774
|
-
def time_of_occurrence(occurrence)
|
775
|
-
time = occurrence.get_text('event_time')
|
776
|
-
date = occurrence.get_text('event-date')
|
777
|
-
time_zone = occurrence.get_text('event-date')
|
778
|
-
DateTime.parse "#{date} #{time} #{time_zone}"
|
779
|
-
end
|
780
|
-
end
|
781
|
-
|
782
|
-
module CPPWSErrorResponse
|
783
|
-
attr_accessor :error_code
|
784
|
-
def handle_error(message, options)
|
785
|
-
@error_code = options[:code]
|
786
|
-
end
|
787
|
-
end
|
788
|
-
|
789
|
-
class CPPWSRateResponse < RateResponse
|
790
|
-
include CPPWSErrorResponse
|
791
|
-
|
792
|
-
def initialize(success, message, params = {}, options = {})
|
793
|
-
handle_error(message, options)
|
794
|
-
super
|
795
|
-
end
|
796
|
-
end
|
797
|
-
|
798
|
-
class CPPWSTrackingResponse < TrackingResponse
|
799
|
-
DELIVERED_EVENT_CODES = %w(1496 1498 1499 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438)
|
800
|
-
include CPPWSErrorResponse
|
801
|
-
|
802
|
-
attr_reader :service_name, :expected_date, :changed_date, :change_reason, :customer_number
|
803
|
-
|
804
|
-
def initialize(success, message, params = {}, options = {})
|
805
|
-
handle_error(message, options)
|
806
|
-
super
|
807
|
-
@service_name = options[:service_name]
|
808
|
-
@expected_date = options[:expected_date]
|
809
|
-
@changed_date = options[:changed_date]
|
810
|
-
@change_reason = options[:change_reason]
|
811
|
-
@customer_number = options[:customer_number]
|
812
|
-
end
|
813
|
-
|
814
|
-
def delivered?
|
815
|
-
!delivered_event.nil?
|
816
|
-
end
|
817
|
-
|
818
|
-
def actual_delivery_time
|
819
|
-
delivered_event.time if delivered?
|
820
|
-
end
|
821
|
-
|
822
|
-
private
|
823
|
-
|
824
|
-
def delivered_event
|
825
|
-
@delivered_event ||= @shipment_events.detect { |event| DELIVERED_EVENT_CODES.include? event.name }
|
826
|
-
end
|
827
|
-
end
|
828
|
-
|
829
|
-
class CPPWSShippingResponse < ShippingResponse
|
830
|
-
include CPPWSErrorResponse
|
831
|
-
attr_reader :label_url, :details_url, :receipt_url
|
832
|
-
def initialize(success, message, params = {}, options = {})
|
833
|
-
handle_error(message, options)
|
834
|
-
super
|
835
|
-
@label_url = options[:label_url]
|
836
|
-
@details_url = options[:details_url]
|
837
|
-
@receipt_url = options[:receipt_url]
|
838
|
-
end
|
839
|
-
end
|
840
|
-
|
841
|
-
class CPPWSRegisterResponse < Response
|
842
|
-
include CPPWSErrorResponse
|
843
|
-
attr_reader :token_id
|
844
|
-
def initialize(success, message, params = {}, options = {})
|
845
|
-
handle_error(message, options)
|
846
|
-
super
|
847
|
-
@token_id = options[:token_id]
|
848
|
-
end
|
849
|
-
|
850
|
-
def redirect_url(customer_id, return_url)
|
851
|
-
"http://www.canadapost.ca/cpotools/apps/drc/merchant?return-url=#{CGI::escape(return_url)}&token-id=#{token_id}&platform-id=#{customer_id}"
|
852
|
-
end
|
853
|
-
end
|
854
|
-
|
855
|
-
class CPPWSMerchantDetailsResponse < Response
|
856
|
-
include CPPWSErrorResponse
|
857
|
-
attr_reader :customer_number, :contract_number, :username, :password, :has_default_credit_card
|
858
|
-
def initialize(success, message, params = {}, options = {})
|
859
|
-
handle_error(message, options)
|
860
|
-
super
|
861
|
-
@customer_number = options[:customer_number]
|
862
|
-
@contract_number = options[:contract_number]
|
863
|
-
@username = options[:username]
|
864
|
-
@password = options[:password]
|
865
|
-
@has_default_credit_card = options[:has_default_credit_card]
|
866
|
-
end
|
867
|
-
end
|
868
|
-
|
869
|
-
class InvalidPinFormatError < StandardError; end
|
870
|
-
class MissingCustomerNumberError < StandardError; end
|
871
|
-
class MissingShippingNumberError < StandardError; end
|
872
|
-
class MissingTokenIdError < StandardError; end
|
873
|
-
end
|
874
|
-
end
|