benhutton-active_shipping 0.9.13
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.
- data/CHANGELOG +38 -0
- data/MIT-LICENSE +20 -0
- data/README.markdown +142 -0
- data/lib/active_merchant/common.rb +14 -0
- data/lib/active_merchant/common/connection.rb +177 -0
- data/lib/active_merchant/common/country.rb +328 -0
- data/lib/active_merchant/common/error.rb +26 -0
- data/lib/active_merchant/common/post_data.rb +24 -0
- data/lib/active_merchant/common/posts_data.rb +63 -0
- data/lib/active_merchant/common/requires_parameters.rb +16 -0
- data/lib/active_merchant/common/utils.rb +22 -0
- data/lib/active_merchant/common/validateable.rb +76 -0
- data/lib/active_shipping.rb +49 -0
- data/lib/active_shipping/shipping/base.rb +13 -0
- data/lib/active_shipping/shipping/carrier.rb +70 -0
- data/lib/active_shipping/shipping/carriers.rb +20 -0
- data/lib/active_shipping/shipping/carriers/bogus_carrier.rb +16 -0
- data/lib/active_shipping/shipping/carriers/canada_post.rb +268 -0
- data/lib/active_shipping/shipping/carriers/fedex.rb +331 -0
- data/lib/active_shipping/shipping/carriers/kunaki.rb +165 -0
- data/lib/active_shipping/shipping/carriers/new_zealand_post.rb +139 -0
- data/lib/active_shipping/shipping/carriers/shipwire.rb +172 -0
- data/lib/active_shipping/shipping/carriers/ups.rb +390 -0
- data/lib/active_shipping/shipping/carriers/usps.rb +441 -0
- data/lib/active_shipping/shipping/location.rb +109 -0
- data/lib/active_shipping/shipping/package.rb +147 -0
- data/lib/active_shipping/shipping/rate_estimate.rb +54 -0
- data/lib/active_shipping/shipping/rate_response.rb +19 -0
- data/lib/active_shipping/shipping/response.rb +46 -0
- data/lib/active_shipping/shipping/shipment_event.rb +14 -0
- data/lib/active_shipping/shipping/tracking_response.rb +22 -0
- data/lib/active_shipping/version.rb +3 -0
- data/lib/certs/cacert.pem +7815 -0
- data/lib/certs/eParcel.dtd +111 -0
- data/lib/vendor/quantified/MIT-LICENSE +22 -0
- data/lib/vendor/quantified/README.markdown +49 -0
- data/lib/vendor/quantified/Rakefile +21 -0
- data/lib/vendor/quantified/init.rb +0 -0
- data/lib/vendor/quantified/lib/quantified.rb +8 -0
- data/lib/vendor/quantified/lib/quantified/attribute.rb +208 -0
- data/lib/vendor/quantified/lib/quantified/length.rb +20 -0
- data/lib/vendor/quantified/lib/quantified/mass.rb +19 -0
- data/lib/vendor/quantified/test/length_test.rb +92 -0
- data/lib/vendor/quantified/test/mass_test.rb +88 -0
- data/lib/vendor/quantified/test/test_helper.rb +2 -0
- data/lib/vendor/test_helper.rb +13 -0
- data/lib/vendor/xml_node/README +36 -0
- data/lib/vendor/xml_node/Rakefile +21 -0
- data/lib/vendor/xml_node/benchmark/bench_generation.rb +32 -0
- data/lib/vendor/xml_node/init.rb +1 -0
- data/lib/vendor/xml_node/lib/xml_node.rb +222 -0
- data/lib/vendor/xml_node/test/test_generating.rb +94 -0
- data/lib/vendor/xml_node/test/test_parsing.rb +43 -0
- metadata +125 -0
@@ -0,0 +1,26 @@
|
|
1
|
+
module ActiveMerchant #:nodoc:
|
2
|
+
class ActiveMerchantError < StandardError #:nodoc:
|
3
|
+
end
|
4
|
+
|
5
|
+
class ConnectionError < ActiveMerchantError # :nodoc:
|
6
|
+
end
|
7
|
+
|
8
|
+
class RetriableConnectionError < ConnectionError # :nodoc:
|
9
|
+
end
|
10
|
+
|
11
|
+
class ResponseError < ActiveMerchantError # :nodoc:
|
12
|
+
attr_reader :response
|
13
|
+
|
14
|
+
def initialize(response, message = nil)
|
15
|
+
@response = response
|
16
|
+
@message = message
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_s
|
20
|
+
"Failed with #{response.code} #{response.message if response.respond_to?(:message)}"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class ClientCertificateError < ActiveMerchantError # :nodoc
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
|
3
|
+
module ActiveMerchant
|
4
|
+
class PostData < Hash
|
5
|
+
class_inheritable_accessor :required_fields, :instance_writer => false
|
6
|
+
self.required_fields = []
|
7
|
+
|
8
|
+
def []=(key, value)
|
9
|
+
return if value.blank? && !required?(key)
|
10
|
+
super
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_post_data
|
14
|
+
collect { |key, value| "#{key}=#{CGI.escape(value.to_s)}" }.join("&")
|
15
|
+
end
|
16
|
+
|
17
|
+
alias_method :to_s, :to_post_data
|
18
|
+
|
19
|
+
private
|
20
|
+
def required?(key)
|
21
|
+
required_fields.include?(key)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module ActiveMerchant #:nodoc:
|
2
|
+
module PostsData #:nodoc:
|
3
|
+
|
4
|
+
def self.included(base)
|
5
|
+
base.superclass_delegating_accessor :ssl_strict
|
6
|
+
base.ssl_strict = true
|
7
|
+
|
8
|
+
base.class_inheritable_accessor :retry_safe
|
9
|
+
base.retry_safe = false
|
10
|
+
|
11
|
+
base.superclass_delegating_accessor :open_timeout
|
12
|
+
base.open_timeout = 60
|
13
|
+
|
14
|
+
base.superclass_delegating_accessor :read_timeout
|
15
|
+
base.read_timeout = 60
|
16
|
+
|
17
|
+
base.superclass_delegating_accessor :logger
|
18
|
+
base.superclass_delegating_accessor :wiredump_device
|
19
|
+
end
|
20
|
+
|
21
|
+
def ssl_get(endpoint, headers={})
|
22
|
+
ssl_request(:get, endpoint, nil, headers)
|
23
|
+
end
|
24
|
+
|
25
|
+
def ssl_post(endpoint, data, headers = {})
|
26
|
+
ssl_request(:post, endpoint, data, headers)
|
27
|
+
end
|
28
|
+
|
29
|
+
def ssl_request(method, endpoint, data, headers)
|
30
|
+
handle_response(raw_ssl_request(method, endpoint, data, headers))
|
31
|
+
end
|
32
|
+
|
33
|
+
def raw_ssl_request(method, endpoint, data, headers = {})
|
34
|
+
connection = Connection.new(endpoint)
|
35
|
+
connection.open_timeout = open_timeout
|
36
|
+
connection.read_timeout = read_timeout
|
37
|
+
connection.retry_safe = retry_safe
|
38
|
+
connection.verify_peer = ssl_strict
|
39
|
+
connection.logger = logger
|
40
|
+
connection.tag = self.class.name
|
41
|
+
connection.wiredump_device = wiredump_device
|
42
|
+
|
43
|
+
connection.pem = @options[:pem] if @options
|
44
|
+
connection.pem_password = @options[:pem_password] if @options
|
45
|
+
|
46
|
+
connection.ignore_http_status = @options[:ignore_http_status] if @options
|
47
|
+
|
48
|
+
connection.request(method, data, headers)
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def handle_response(response)
|
54
|
+
case response.code.to_i
|
55
|
+
when 200...300
|
56
|
+
response.body
|
57
|
+
else
|
58
|
+
raise ResponseError.new(response)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module ActiveMerchant #:nodoc:
|
2
|
+
module RequiresParameters #:nodoc:
|
3
|
+
def requires!(hash, *params)
|
4
|
+
params.each do |param|
|
5
|
+
if param.is_a?(Array)
|
6
|
+
raise ArgumentError.new("Missing required parameter: #{param.first}") unless hash.has_key?(param.first)
|
7
|
+
|
8
|
+
valid_options = param[1..-1]
|
9
|
+
raise ArgumentError.new("Parameter: #{param.first} must be one of #{valid_options.to_sentence(:words_connector => 'or')}") unless valid_options.include?(hash[param.first])
|
10
|
+
else
|
11
|
+
raise ArgumentError.new("Missing required parameter: #{param}") unless hash.has_key?(param)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'digest/md5'
|
2
|
+
|
3
|
+
module ActiveMerchant #:nodoc:
|
4
|
+
module Utils #:nodoc:
|
5
|
+
def generate_unique_id
|
6
|
+
md5 = Digest::MD5.new
|
7
|
+
now = Time.now
|
8
|
+
md5 << now.to_s
|
9
|
+
md5 << String(now.usec)
|
10
|
+
md5 << String(rand(0))
|
11
|
+
md5 << String($$)
|
12
|
+
md5 << self.class.name
|
13
|
+
md5.hexdigest
|
14
|
+
end
|
15
|
+
|
16
|
+
module_function :generate_unique_id
|
17
|
+
|
18
|
+
def deprecated(message)
|
19
|
+
warn(Kernel.caller[1] + message)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
module ActiveMerchant #:nodoc:
|
2
|
+
module Validateable #:nodoc:
|
3
|
+
def valid?
|
4
|
+
errors.clear
|
5
|
+
|
6
|
+
before_validate if respond_to?(:before_validate, true)
|
7
|
+
validate if respond_to?(:validate, true)
|
8
|
+
|
9
|
+
errors.empty?
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(attributes = {})
|
13
|
+
self.attributes = attributes
|
14
|
+
end
|
15
|
+
|
16
|
+
def errors
|
17
|
+
@errors ||= Errors.new(self)
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def attributes=(attributes)
|
23
|
+
unless attributes.nil?
|
24
|
+
for key, value in attributes
|
25
|
+
send("#{key}=", value )
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# This hash keeps the errors of the object
|
31
|
+
class Errors < HashWithIndifferentAccess
|
32
|
+
|
33
|
+
def initialize(base)
|
34
|
+
@base = base
|
35
|
+
end
|
36
|
+
|
37
|
+
def count
|
38
|
+
size
|
39
|
+
end
|
40
|
+
|
41
|
+
# returns a specific fields error message.
|
42
|
+
# if more than one error is available we will only return the first. If no error is available
|
43
|
+
# we return an empty string
|
44
|
+
def on(field)
|
45
|
+
self[field].to_a.first
|
46
|
+
end
|
47
|
+
|
48
|
+
def add(field, error)
|
49
|
+
self[field] ||= []
|
50
|
+
self[field] << error
|
51
|
+
end
|
52
|
+
|
53
|
+
def add_to_base(error)
|
54
|
+
add(:base, error)
|
55
|
+
end
|
56
|
+
|
57
|
+
def each_full
|
58
|
+
full_messages.each { |msg| yield msg }
|
59
|
+
end
|
60
|
+
|
61
|
+
def full_messages
|
62
|
+
result = []
|
63
|
+
|
64
|
+
self.each do |key, messages|
|
65
|
+
if key == 'base'
|
66
|
+
result << "#{messages.first}"
|
67
|
+
else
|
68
|
+
result << "#{key.to_s.humanize} #{messages.first}"
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
result
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (c) 2009 Jaded Pixel
|
3
|
+
#
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
5
|
+
# a copy of this software and associated documentation files (the
|
6
|
+
# "Software"), to deal in the Software without restriction, including
|
7
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
8
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
9
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
10
|
+
# the following conditions:
|
11
|
+
#
|
12
|
+
# The above copyright notice and this permission notice shall be
|
13
|
+
# included in all copies or substantial portions of the Software.
|
14
|
+
#
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
17
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
19
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
20
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
21
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
|
+
#++
|
23
|
+
|
24
|
+
$:.unshift File.dirname(__FILE__)
|
25
|
+
|
26
|
+
begin
|
27
|
+
require 'active_support/all'
|
28
|
+
rescue LoadError => e
|
29
|
+
require 'rubygems'
|
30
|
+
gem "activesupport", ">= 2.3.5"
|
31
|
+
require "active_support/all"
|
32
|
+
end
|
33
|
+
|
34
|
+
autoload :XmlNode, 'vendor/xml_node/lib/xml_node'
|
35
|
+
autoload :Quantified, 'vendor/quantified/lib/quantified'
|
36
|
+
|
37
|
+
require 'net/https'
|
38
|
+
require 'active_merchant/common'
|
39
|
+
|
40
|
+
require 'active_shipping/shipping/base'
|
41
|
+
require 'active_shipping/shipping/response'
|
42
|
+
require 'active_shipping/shipping/rate_response'
|
43
|
+
require 'active_shipping/shipping/tracking_response'
|
44
|
+
require 'active_shipping/shipping/package'
|
45
|
+
require 'active_shipping/shipping/location'
|
46
|
+
require 'active_shipping/shipping/rate_estimate'
|
47
|
+
require 'active_shipping/shipping/shipment_event'
|
48
|
+
require 'active_shipping/shipping/carrier'
|
49
|
+
require 'active_shipping/shipping/carriers'
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module ActiveMerchant
|
2
|
+
module Shipping
|
3
|
+
module Base
|
4
|
+
mattr_accessor :mode
|
5
|
+
self.mode = :production
|
6
|
+
|
7
|
+
def self.carrier(name)
|
8
|
+
ActiveMerchant::Shipping::Carriers.all.find {|c| c.name.downcase == name.to_s.downcase} ||
|
9
|
+
raise(NameError, "unknown carrier #{name}")
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module ActiveMerchant
|
2
|
+
module Shipping
|
3
|
+
class Carrier
|
4
|
+
|
5
|
+
include RequiresParameters
|
6
|
+
include PostsData
|
7
|
+
include Quantified
|
8
|
+
|
9
|
+
attr_reader :last_request
|
10
|
+
attr_accessor :test_mode
|
11
|
+
alias_method :test_mode?, :test_mode
|
12
|
+
|
13
|
+
# Credentials should be in options hash under keys :login, :password and/or :key.
|
14
|
+
def initialize(options = {})
|
15
|
+
requirements.each {|key| requires!(options, key)}
|
16
|
+
@options = options
|
17
|
+
@last_request = nil
|
18
|
+
@test_mode = @options[:test]
|
19
|
+
end
|
20
|
+
|
21
|
+
# Override to return required keys in options hash for initialize method.
|
22
|
+
def requirements
|
23
|
+
[]
|
24
|
+
end
|
25
|
+
|
26
|
+
# Override with whatever you need to get the rates
|
27
|
+
def find_rates(origin, destination, packages, options = {})
|
28
|
+
end
|
29
|
+
|
30
|
+
# Validate credentials with a call to the API. By default this just does a find_rates call
|
31
|
+
# with the orgin and destination both as the carrier's default_location. Override to provide
|
32
|
+
# alternate functionality, such as checking for test_mode to use test servers, etc.
|
33
|
+
def valid_credentials?
|
34
|
+
location = self.class.default_location
|
35
|
+
find_rates(location,location,Package.new(100, [5,15,30]), :test => test_mode)
|
36
|
+
rescue ActiveMerchant::Shipping::ResponseError
|
37
|
+
false
|
38
|
+
else
|
39
|
+
true
|
40
|
+
end
|
41
|
+
|
42
|
+
def maximum_weight
|
43
|
+
Mass.new(150, :pounds)
|
44
|
+
end
|
45
|
+
|
46
|
+
protected
|
47
|
+
|
48
|
+
def node_text_or_nil(xml_node)
|
49
|
+
xml_node ? xml_node.text : nil
|
50
|
+
end
|
51
|
+
|
52
|
+
# Override in subclasses for non-U.S.-based carriers.
|
53
|
+
def self.default_location
|
54
|
+
Location.new( :country => 'US',
|
55
|
+
:state => 'CA',
|
56
|
+
:city => 'Beverly Hills',
|
57
|
+
:address1 => '455 N. Rexford Dr.',
|
58
|
+
:address2 => '3rd Floor',
|
59
|
+
:zip => '90210',
|
60
|
+
:phone => '1-310-285-1013',
|
61
|
+
:fax => '1-310-275-8159')
|
62
|
+
end
|
63
|
+
|
64
|
+
# Use after building the request to save for later inspection. Probably won't ever be overridden.
|
65
|
+
def save_request(r)
|
66
|
+
@last_request = r
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'active_shipping/shipping/carriers/bogus_carrier'
|
2
|
+
require 'active_shipping/shipping/carriers/ups'
|
3
|
+
require 'active_shipping/shipping/carriers/usps'
|
4
|
+
require 'active_shipping/shipping/carriers/fedex'
|
5
|
+
require 'active_shipping/shipping/carriers/shipwire'
|
6
|
+
require 'active_shipping/shipping/carriers/kunaki'
|
7
|
+
require 'active_shipping/shipping/carriers/canada_post'
|
8
|
+
require 'active_shipping/shipping/carriers/new_zealand_post'
|
9
|
+
|
10
|
+
module ActiveMerchant
|
11
|
+
module Shipping
|
12
|
+
module Carriers
|
13
|
+
class <<self
|
14
|
+
def all
|
15
|
+
[BogusCarrier, UPS, USPS, FedEx, Shipwire, Kunaki, CanadaPost, NewZealandPost]
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module ActiveMerchant
|
2
|
+
module Shipping
|
3
|
+
class BogusCarrier < Carrier
|
4
|
+
cattr_reader :name
|
5
|
+
@@name = "Bogus Carrier"
|
6
|
+
|
7
|
+
|
8
|
+
def find_rates(origin, destination, packages, options = {})
|
9
|
+
origin = Location.from(origin)
|
10
|
+
destination = Location.from(destination)
|
11
|
+
packages = Array(packages)
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,268 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
|
3
|
+
module ActiveMerchant
|
4
|
+
module Shipping
|
5
|
+
|
6
|
+
class CanadaPost < Carrier
|
7
|
+
|
8
|
+
# NOTE!
|
9
|
+
# A Merchant CPC Id must be assigned to you by Canada Post
|
10
|
+
# CPC_DEMO_XML is just a public domain account for testing
|
11
|
+
|
12
|
+
class CanadaPostRateResponse < RateResponse
|
13
|
+
|
14
|
+
attr_reader :boxes, :postal_outlets
|
15
|
+
|
16
|
+
def initialize(success, message, params = {}, options = {})
|
17
|
+
@boxes = options[:boxes]
|
18
|
+
@postal_outlets = options[:postal_outlets]
|
19
|
+
super
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
cattr_reader :name, :name_french
|
25
|
+
@@name = "Canada Post"
|
26
|
+
@@name_french = "Postes Canada"
|
27
|
+
|
28
|
+
Box = Struct.new(:name, :weight, :expediter_weight, :length, :width, :height, :packedItems)
|
29
|
+
PackedItem = Struct.new(:quantity, :description)
|
30
|
+
PostalOutlet = Struct.new(:sequence_no, :distance, :name, :business_name, :postal_address, :business_hours)
|
31
|
+
|
32
|
+
DEFAULT_TURN_AROUND_TIME = 24
|
33
|
+
URL = "http://sellonline.canadapost.ca:30000"
|
34
|
+
DOCTYPE = '<!DOCTYPE eparcel SYSTEM "http://sellonline.canadapost.ca/DevelopersResources/protocolV3/eParcel.dtd">'
|
35
|
+
|
36
|
+
RESPONSE_CODES = {
|
37
|
+
'1' => "All calculation was done",
|
38
|
+
'2' => "Default shipping rates are returned due to a problem during the processing of the request.",
|
39
|
+
'-2' => "Missing argument when calling module",
|
40
|
+
'-5' => "No Item to ship",
|
41
|
+
'-6' => "Illegal Item weight",
|
42
|
+
'-7' => "Illegal item dimension",
|
43
|
+
'-12' => "Can't open IM config file",
|
44
|
+
'-13' => "Can't create log files",
|
45
|
+
'-15' => "Invalid config file format",
|
46
|
+
'-102' => "Invalid socket connection",
|
47
|
+
'-106' => "Can't connect to server",
|
48
|
+
'-1000' => "Unknow request type sent by client",
|
49
|
+
'-1002' => "MAS Timed out",
|
50
|
+
'-1004' => "Socket communication break",
|
51
|
+
'-1005' => "Did not receive required data on socket.",
|
52
|
+
'-2000' => "Unable to estabish socket connection with RSSS",
|
53
|
+
'-2001' => "Merchant Id not found on server",
|
54
|
+
'-2002' => "One or more parameter was not sent by the IM to the MAS",
|
55
|
+
'-2003' => "Did not receive required data on socket.",
|
56
|
+
'-2004' => "The request contains to many items to process it.",
|
57
|
+
'-2005' => "The request received on socket is larger than the maximum allowed.",
|
58
|
+
'-3000' => "Origin Postal Code is illegal",
|
59
|
+
'-3001' => "Destination Postal Code/State Name/ Country is illegal",
|
60
|
+
'-3002' => "Parcel too large to be shipped with CPC",
|
61
|
+
'-3003' => "Parcel too small to be shipped with CPC",
|
62
|
+
'-3004' => "Parcel too heavy to be shipped with CPC",
|
63
|
+
'-3005' => "Internal error code returned by the rating DLL",
|
64
|
+
'-3006' => "The pick up time format is invalid or not defined.",
|
65
|
+
'-4000' => "Volumetric internal error",
|
66
|
+
'-4001' => "Volumetric time out calculation error.",
|
67
|
+
'-4002' => "No bins provided to the volumetric engine.",
|
68
|
+
'-4003' => "No items provided to the volumetric engine.",
|
69
|
+
'-4004' => "Item is too large to be packed",
|
70
|
+
'-4005' => "Number of item more than maximum allowed",
|
71
|
+
'-5000' => "XML Parsing error",
|
72
|
+
'-5001' => "XML Tag not found",
|
73
|
+
'-5002' => "Node Value Number format error",
|
74
|
+
'-5003' => "Node value is empty",
|
75
|
+
'-5004' => "Unable to create/parse XML Document",
|
76
|
+
'-6000' => "Unable to open the database",
|
77
|
+
'-6001' => "Unable to read from the database",
|
78
|
+
'-6002' => "Unable to write to the database",
|
79
|
+
'-50000' => "Internal problem - Please contact Sell Online Help Desk"
|
80
|
+
}
|
81
|
+
|
82
|
+
NON_ISO_COUNTRY_NAMES = {
|
83
|
+
'Russian Federation' => 'Russia'
|
84
|
+
}
|
85
|
+
|
86
|
+
|
87
|
+
def requirements
|
88
|
+
[:login]
|
89
|
+
end
|
90
|
+
|
91
|
+
def find_rates(origin, destination, line_items = [], options = {})
|
92
|
+
rate_request = build_rate_request(origin, destination, line_items, options)
|
93
|
+
commit(rate_request, origin, destination, options)
|
94
|
+
end
|
95
|
+
|
96
|
+
def maximum_weight
|
97
|
+
Mass.new(30, :kilograms)
|
98
|
+
end
|
99
|
+
|
100
|
+
def self.default_location
|
101
|
+
{
|
102
|
+
:country => 'CA',
|
103
|
+
:province => 'ON',
|
104
|
+
:city => 'Ottawa',
|
105
|
+
:address1 => '61A York St',
|
106
|
+
:postal_code => 'K1N5T2'
|
107
|
+
}
|
108
|
+
end
|
109
|
+
|
110
|
+
protected
|
111
|
+
|
112
|
+
def commit(request, origin, destination, options = {})
|
113
|
+
response = parse_rate_response(ssl_post(URL, request), origin, destination, options)
|
114
|
+
end
|
115
|
+
|
116
|
+
private
|
117
|
+
|
118
|
+
def build_rate_request(origin, destination, line_items = [], options = {})
|
119
|
+
line_items = [line_items] if !line_items.is_a?(Array)
|
120
|
+
origin = origin.is_a?(Location) ? origin : Location.new(origin)
|
121
|
+
destination = destination.is_a?(Location) ? destination : Location.new(destination)
|
122
|
+
|
123
|
+
xml_request = XmlNode.new('eparcel') do |root_node|
|
124
|
+
root_node << XmlNode.new('language', @options[:french] ? 'fr' : 'en')
|
125
|
+
root_node << XmlNode.new('ratesAndServicesRequest') do |request|
|
126
|
+
|
127
|
+
request << XmlNode.new('merchantCPCID', @options[:login])
|
128
|
+
request << XmlNode.new('fromPostalCode', origin.postal_code)
|
129
|
+
request << XmlNode.new('turnAroundTime', options[:turn_around_time] ? options[:turn_around_time] : DEFAULT_TURN_AROUND_TIME)
|
130
|
+
request << XmlNode.new('itemsPrice', dollar_amount(line_items.sum(&:value)))
|
131
|
+
|
132
|
+
#line items
|
133
|
+
request << build_line_items(line_items)
|
134
|
+
|
135
|
+
#delivery info
|
136
|
+
#NOTE: These tags MUST be after line items
|
137
|
+
request << XmlNode.new('city', destination.city)
|
138
|
+
request << XmlNode.new('provOrState', destination.province)
|
139
|
+
request << XmlNode.new('country', handle_non_iso_country_names(destination.country))
|
140
|
+
request << XmlNode.new('postalCode', destination.postal_code)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
DOCTYPE + xml_request.to_s
|
145
|
+
end
|
146
|
+
|
147
|
+
def parse_rate_response(response, origin, destination, options = {})
|
148
|
+
xml = REXML::Document.new(response)
|
149
|
+
success = response_success?(xml)
|
150
|
+
message = response_message(xml)
|
151
|
+
|
152
|
+
rate_estimates = []
|
153
|
+
boxes = []
|
154
|
+
if success
|
155
|
+
xml.elements.each('eparcel/ratesAndServicesResponse/product') do |product|
|
156
|
+
service_name = (@options[:french] ? @@name_french : @@name) + product.get_text('name').to_s
|
157
|
+
service_code = product.attribute('id').to_s
|
158
|
+
delivery_date = date_for(product.get_text('deliveryDate').to_s)
|
159
|
+
|
160
|
+
rate_estimates << RateEstimate.new(origin, destination, @@name, service_name,
|
161
|
+
:service_code => service_code,
|
162
|
+
:total_price => product.get_text('rate').to_s,
|
163
|
+
:delivery_date => delivery_date,
|
164
|
+
:currency => 'CAD'
|
165
|
+
)
|
166
|
+
end
|
167
|
+
|
168
|
+
boxes = xml.elements.collect('eparcel/ratesAndServicesResponse/packing/box') do |box|
|
169
|
+
b = Box.new
|
170
|
+
b.packedItems = []
|
171
|
+
b.name = box.get_text('name').to_s
|
172
|
+
b.weight = box.get_text('weight').to_s.to_f
|
173
|
+
b.expediter_weight = box.get_text('expediterWeight').to_s.to_f
|
174
|
+
b.length = box.get_text('length').to_s.to_f
|
175
|
+
b.width = box.get_text('width').to_s.to_f
|
176
|
+
b.height = box.get_text('height').to_s.to_f
|
177
|
+
b.packedItems = box.elements.collect('packedItem') do |item|
|
178
|
+
p = PackedItem.new
|
179
|
+
p.quantity = item.get_text('quantity').to_s.to_i
|
180
|
+
p.description = item.get_text('description').to_s
|
181
|
+
p
|
182
|
+
end
|
183
|
+
b
|
184
|
+
end
|
185
|
+
|
186
|
+
postal_outlets = xml.elements.collect('eparcel/ratesAndServicesResponse/nearestPostalOutlet') do |outlet|
|
187
|
+
postal_outlet = PostalOutlet.new
|
188
|
+
postal_outlet.sequence_no = outlet.get_text('postalOutletSequenceNo').to_s
|
189
|
+
postal_outlet.distance = outlet.get_text('distance').to_s
|
190
|
+
postal_outlet.name = outlet.get_text('outletName').to_s
|
191
|
+
postal_outlet.business_name = outlet.get_text('businessName').to_s
|
192
|
+
|
193
|
+
postal_outlet.postal_address = Location.new({
|
194
|
+
:address1 => outlet.get_text('postalAddress/addressLine').to_s,
|
195
|
+
:postal_code => outlet.get_text('postalAddress/postal_code').to_s,
|
196
|
+
:city => outlet.get_text('postalAddress/municipality').to_s,
|
197
|
+
:province => outlet.get_text('postalAddress/province').to_s,
|
198
|
+
:country => 'Canada',
|
199
|
+
:phone_number => outlet.get_text('phoneNumber').to_s
|
200
|
+
})
|
201
|
+
|
202
|
+
postal_outlet.business_hours = outlet.elements.collect('businessHours') do |hour|
|
203
|
+
{ :day_of_week => hour.get_text('dayOfWeek').to_s, :time => hour.get_text('time').to_s }
|
204
|
+
end
|
205
|
+
|
206
|
+
postal_outlet
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
CanadaPostRateResponse.new(success, message, Hash.from_xml(response), :rates => rate_estimates, :xml => response, :boxes => boxes, :postal_outlets => postal_outlets)
|
211
|
+
end
|
212
|
+
|
213
|
+
def date_for(string)
|
214
|
+
string && Time.parse(string)
|
215
|
+
rescue ArgumentError
|
216
|
+
nil
|
217
|
+
end
|
218
|
+
|
219
|
+
def response_success?(xml)
|
220
|
+
value = xml.get_text('eparcel/ratesAndServicesResponse/statusCode').to_s
|
221
|
+
value == '1' || value == '2'
|
222
|
+
end
|
223
|
+
|
224
|
+
def response_message(xml)
|
225
|
+
xml.get_text('eparcel/ratesAndServicesResponse/statusMessage').to_s
|
226
|
+
end
|
227
|
+
|
228
|
+
# <!-- List of items in the shopping -->
|
229
|
+
# <!-- cart -->
|
230
|
+
# <!-- Each item is defined by : -->
|
231
|
+
# <!-- - quantity (mandatory) -->
|
232
|
+
# <!-- - size (mandatory) -->
|
233
|
+
# <!-- - weight (mandatory) -->
|
234
|
+
# <!-- - description (mandatory) -->
|
235
|
+
# <!-- - ready to ship (optional) -->
|
236
|
+
|
237
|
+
def build_line_items(line_items)
|
238
|
+
xml_line_items = XmlNode.new('lineItems') do |line_items_node|
|
239
|
+
|
240
|
+
line_items.each do |line_item|
|
241
|
+
|
242
|
+
line_items_node << XmlNode.new('item') do |item|
|
243
|
+
item << XmlNode.new('quantity', 1)
|
244
|
+
item << XmlNode.new('weight', line_item.kilograms)
|
245
|
+
item << XmlNode.new('length', line_item.cm(:length).to_s)
|
246
|
+
item << XmlNode.new('width', line_item.cm(:width).to_s)
|
247
|
+
item << XmlNode.new('height', line_item.cm(:height).to_s)
|
248
|
+
item << XmlNode.new('description', line_item.options[:description] || ' ')
|
249
|
+
item << XmlNode.new('readyToShip', line_item.options[:ready_to_ship] || nil)
|
250
|
+
|
251
|
+
# By setting the 'readyToShip' tag to true, Sell Online will not pack this item in the boxes defined in the merchant profile.
|
252
|
+
end
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
xml_line_items
|
257
|
+
end
|
258
|
+
|
259
|
+
def dollar_amount(cents)
|
260
|
+
"%0.2f" % (cents / 100.0)
|
261
|
+
end
|
262
|
+
|
263
|
+
def handle_non_iso_country_names(country)
|
264
|
+
NON_ISO_COUNTRY_NAMES[country.to_s] || country
|
265
|
+
end
|
266
|
+
end
|
267
|
+
end
|
268
|
+
end
|