benhutton-active_shipping 0.9.13
Sign up to get free protection for your applications and to get access to all the features.
- 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
|