kschadeck-active_shipping 0.9.15
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 +126 -0
- data/lib/active_shipping.rb +50 -0
- data/lib/active_shipping/shipping/base.rb +13 -0
- data/lib/active_shipping/shipping/carrier.rb +81 -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 +261 -0
- data/lib/active_shipping/shipping/carriers/fedex.rb +372 -0
- data/lib/active_shipping/shipping/carriers/kunaki.rb +165 -0
- data/lib/active_shipping/shipping/carriers/new_zealand_post.rb +269 -0
- data/lib/active_shipping/shipping/carriers/shipwire.rb +178 -0
- data/lib/active_shipping/shipping/carriers/ups.rb +452 -0
- data/lib/active_shipping/shipping/carriers/usps.rb +441 -0
- data/lib/active_shipping/shipping/location.rb +149 -0
- data/lib/active_shipping/shipping/package.rb +147 -0
- data/lib/active_shipping/shipping/rate_estimate.rb +62 -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/shipment_packer.rb +48 -0
- data/lib/active_shipping/shipping/tracking_response.rb +47 -0
- data/lib/active_shipping/version.rb +3 -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 +6 -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 +10 -0
- data/lib/vendor/test_helper.rb +7 -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 +233 -0
data/CHANGELOG
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
2011/04/21:
|
2
|
+
* USPS updated to use new APIs [james]
|
3
|
+
* new :gift boolean option for Package [james]
|
4
|
+
* Location's :address_type can be "po_box" [james]
|
5
|
+
|
6
|
+
Earlier:
|
7
|
+
* New Zealand Post [AbleTech]
|
8
|
+
* Include address name for rate requests to Shipwire if provided [dennis]
|
9
|
+
* Add support for address name to Location [dennis]
|
10
|
+
* Add fix for updated USPS API to strip any encoded html and trailing asterisks from rate names [dennis]
|
11
|
+
* Add carrier CanadaPost [william]
|
12
|
+
* Update FedEx rates and added ability to auto-generate rate name from code that gets returned by FedEx [dennis]
|
13
|
+
* Assume test_helper is in load path when running tests [cody]
|
14
|
+
* Add support Kunaki rating service [cody]
|
15
|
+
* Require active_support instead of activesupport to avoid deprecation warning in Rails 2.3.5 [cody]
|
16
|
+
* Remove ftools for Rails 1.9 compatibility and remove xml logging, as logging is now included in the connection [cody]
|
17
|
+
* Update connection code from ActiveMerchant [cody]
|
18
|
+
* Fix space-ridden USPS usernames when validating credentials [james]
|
19
|
+
* Remove extra slash from USPS URLs [james]
|
20
|
+
* Update Shipwire endpoint hostname [cody]
|
21
|
+
* Add missing ISO countries [Edward Ocampo-Gooding]
|
22
|
+
* Add support for Guernsey to country.rb [cody]
|
23
|
+
* Use :words_connector instead of connector in RequiresParameters [cody]
|
24
|
+
* Fix extra slash in UPS endpoints [cody]
|
25
|
+
* Add name to Shipwire class [cody]
|
26
|
+
* Improve FedEx handling of some error conditions [cody]
|
27
|
+
* Add support for validating credentials to Shipwire [cody]
|
28
|
+
* Add support for ssl_get to PostsData. Update Carriers to use PostsData module. Turn on retry safety for carriers [cody]
|
29
|
+
* Add support for Shipwire Shipping Rate API [cody]
|
30
|
+
* Cleanup package tests [cody]
|
31
|
+
* Remove unused Carrier#setup method [cody]
|
32
|
+
* Don't use Array splat in Regex comparisons in Package [cody]
|
33
|
+
* Default the Location to use the :alpha2 country code format [cody]
|
34
|
+
* Add configurable timeouts from Active Merchant [cody]
|
35
|
+
* Update xml_node.rb from XML Node [cody]
|
36
|
+
* Update requires_parameters from ActiveMerchant [cody]
|
37
|
+
* Sync posts_data.rb with ActiveMerchant [cody]
|
38
|
+
* Don't use credentials fixtures in local tests [cody]
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2008 James MacAulay
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.markdown
ADDED
@@ -0,0 +1,126 @@
|
|
1
|
+
# Active Shipping
|
2
|
+
|
3
|
+
This library interfaces with the web services of various shipping carriers. The goal is to abstract the features that are most frequently used into a pleasant and consistent Ruby API. Active Shipping is an extension of [Active Merchant][], and as such, it borrows heavily from conventions used in the latter.
|
4
|
+
|
5
|
+
Active Shipping is currently being used and improved in a production environment for [Shopify][]. Development is being done by the Shopify integrations team (<integrations-team@shopify.com>). Discussion is welcome in the [Active Merchant Google Group][discuss].
|
6
|
+
|
7
|
+
[Active Merchant]:http://www.activemerchant.org
|
8
|
+
[Shopify]:http://www.shopify.com
|
9
|
+
[discuss]:http://groups.google.com/group/activemerchant
|
10
|
+
|
11
|
+
## Supported Shipping Carriers
|
12
|
+
|
13
|
+
* [UPS](http://www.ups.com)
|
14
|
+
* [USPS](http://www.usps.com)
|
15
|
+
* [FedEx](http://www.fedex.com)
|
16
|
+
* [Canada Post](http://www.canadapost.ca)
|
17
|
+
* [New Zealand Post](http://www.nzpost.co.nz)
|
18
|
+
* more soon!
|
19
|
+
|
20
|
+
## Installation
|
21
|
+
|
22
|
+
gem install active_shipping
|
23
|
+
|
24
|
+
...or add it to your [Gemfile](http://gembundler.com/).
|
25
|
+
|
26
|
+
## Sample Usage
|
27
|
+
|
28
|
+
### Compare rates from different carriers
|
29
|
+
|
30
|
+
require 'active_shipping'
|
31
|
+
include ActiveMerchant::Shipping
|
32
|
+
|
33
|
+
# Package up a poster and a Wii for your nephew.
|
34
|
+
packages = [
|
35
|
+
Package.new( 100, # 100 grams
|
36
|
+
[93,10], # 93 cm long, 10 cm diameter
|
37
|
+
:cylinder => true), # cylinders have different volume calculations
|
38
|
+
|
39
|
+
Package.new( (7.5 * 16), # 7.5 lbs, times 16 oz/lb.
|
40
|
+
[15, 10, 4.5], # 15x10x4.5 inches
|
41
|
+
:units => :imperial) # not grams, not centimetres
|
42
|
+
]
|
43
|
+
|
44
|
+
# You live in Beverly Hills, he lives in Ottawa
|
45
|
+
origin = Location.new( :country => 'US',
|
46
|
+
:state => 'CA',
|
47
|
+
:city => 'Beverly Hills',
|
48
|
+
:zip => '90210')
|
49
|
+
|
50
|
+
destination = Location.new( :country => 'CA',
|
51
|
+
:province => 'ON',
|
52
|
+
:city => 'Ottawa',
|
53
|
+
:postal_code => 'K1P 1J1')
|
54
|
+
|
55
|
+
# Find out how much it'll be.
|
56
|
+
ups = UPS.new(:login => 'auntjudy', :password => 'secret', :key => 'xml-access-key')
|
57
|
+
response = ups.find_rates(origin, destination, packages)
|
58
|
+
|
59
|
+
ups_rates = response.rates.sort_by(&:price).collect {|rate| [rate.service_name, rate.price]}
|
60
|
+
# => [["UPS Standard", 3936],
|
61
|
+
# ["UPS Worldwide Expedited", 8682],
|
62
|
+
# ["UPS Saver", 9348],
|
63
|
+
# ["UPS Express", 9702],
|
64
|
+
# ["UPS Worldwide Express Plus", 14502]]
|
65
|
+
|
66
|
+
# Check out USPS for comparison...
|
67
|
+
usps = USPS.new(:login => 'developer-key')
|
68
|
+
response = usps.find_rates(origin, destination, packages)
|
69
|
+
|
70
|
+
usps_rates = response.rates.sort_by(&:price).collect {|rate| [rate.service_name, rate.price]}
|
71
|
+
# => [["USPS Priority Mail International", 4110],
|
72
|
+
# ["USPS Express Mail International (EMS)", 5750],
|
73
|
+
# ["USPS Global Express Guaranteed Non-Document Non-Rectangular", 9400],
|
74
|
+
# ["USPS GXG Envelopes", 9400],
|
75
|
+
# ["USPS Global Express Guaranteed Non-Document Rectangular", 9400],
|
76
|
+
# ["USPS Global Express Guaranteed", 9400]]
|
77
|
+
|
78
|
+
### Track a FedEx package
|
79
|
+
|
80
|
+
fedex = FedEx.new(:login => '999999999', :password => '7777777')
|
81
|
+
tracking_info = fedex.find_tracking_info('tracking-number', :carrier_code => 'fedex_ground') # Ground package
|
82
|
+
|
83
|
+
tracking_info.shipment_events.each do |event|
|
84
|
+
puts "#{event.name} at #{event.location.city}, #{event.location.state} on #{event.time}. #{event.message}"
|
85
|
+
end
|
86
|
+
# => Package information transmitted to FedEx at NASHVILLE LOCAL, TN on Thu Oct 23 00:00:00 UTC 2008.
|
87
|
+
# Picked up by FedEx at NASHVILLE LOCAL, TN on Thu Oct 23 17:30:00 UTC 2008.
|
88
|
+
# Scanned at FedEx sort facility at NASHVILLE, TN on Thu Oct 23 18:50:00 UTC 2008.
|
89
|
+
# Departed FedEx sort facility at NASHVILLE, TN on Thu Oct 23 22:33:00 UTC 2008.
|
90
|
+
# Arrived at FedEx sort facility at KNOXVILLE, TN on Fri Oct 24 02:45:00 UTC 2008.
|
91
|
+
# Scanned at FedEx sort facility at KNOXVILLE, TN on Fri Oct 24 05:56:00 UTC 2008.
|
92
|
+
# Delivered at Knoxville, TN on Fri Oct 24 16:45:00 UTC 2008. Signed for by: T.BAKER
|
93
|
+
|
94
|
+
## Running the tests
|
95
|
+
|
96
|
+
After installing dependencies with `bundle install`, you can run the unit tests with `rake test:units` and the remote tests with `rake test:remote`. The unit tests mock out requests and responses so that everything runs locally, while the remote tests actually hit the carrier servers. For the remote tests, you'll need valid test credentials for any carriers' tests you want to run. The credentials should go in ~/.active_merchant/fixtures.yml, and the format of that file can be seen in the included [fixtures.yml](https://github.com/Shopify/active_shipping/blob/master/test/fixtures.yml).
|
97
|
+
|
98
|
+
## Contributing
|
99
|
+
|
100
|
+
Yes, please! Take a look at the tests and the implementation of the Carrier class to see how the basics work. At some point soon there will be a carrier template generator along the lines of the gateway generator included in Active Merchant, but carrier.rb outlines most of what's necessary. The other main classes that would be good to familiarize yourself with are Location, Package, and Response.
|
101
|
+
|
102
|
+
For the features you add, you should have both unit tests and remote tests. It's probably best to start with the remote tests, and then log those requests and responses and use them as the mocks for the unit tests. You can see how this works with the USPS tests right now:
|
103
|
+
|
104
|
+
https://github.com/Shopify/active_shipping/blob/master/test/remote/usps_test.rb
|
105
|
+
https://github.com/Shopify/active_shipping/blob/master/test/unit/carriers/usps_test.rb
|
106
|
+
https://github.com/Shopify/active_shipping/tree/master/test/fixtures/xml/usps
|
107
|
+
|
108
|
+
To log requests and responses, just set the `logger` on your carrier class to some kind of `Logger` object:
|
109
|
+
|
110
|
+
USPS.logger = Logger.new($stdout)
|
111
|
+
|
112
|
+
(This logging functionality is provided by the [`PostsData` module](https://github.com/Shopify/active_utils/blob/master/lib/active_utils/common/posts_data.rb) in the `active_utils` dependency.)
|
113
|
+
|
114
|
+
After you've pushed your well-tested changes to your github fork, make a pull request and we'll take it from there!
|
115
|
+
|
116
|
+
## Contributors
|
117
|
+
|
118
|
+
* James MacAulay (<http://jmacaulay.net>)
|
119
|
+
* Tobias Luetke (<http://blog.leetsoft.com>)
|
120
|
+
* Cody Fauser (<http://codyfauser.com>)
|
121
|
+
* Jimmy Baker (<http://jimmyville.com/>)
|
122
|
+
* William Lang (<http://williamlang.net/>)
|
123
|
+
|
124
|
+
## Legal Mumbo Jumbo
|
125
|
+
|
126
|
+
Unless otherwise noted in specific files, all code in the Active Shipping project is under the copyright and license described in the included MIT-LICENSE file.
|
@@ -0,0 +1,50 @@
|
|
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_utils'
|
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/shipment_packer'
|
49
|
+
require 'active_shipping/shipping/carrier'
|
50
|
+
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,81 @@
|
|
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
|
+
|
69
|
+
def timestamp_from_business_day(days)
|
70
|
+
return unless days
|
71
|
+
date = DateTime.now
|
72
|
+
days.times do
|
73
|
+
begin
|
74
|
+
date = date + 1
|
75
|
+
end until ![0,6].include?(date.wday)
|
76
|
+
end
|
77
|
+
date
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
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,261 @@
|
|
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
|
+
|
159
|
+
rate_estimates << RateEstimate.new(origin, destination, @@name, service_name,
|
160
|
+
:service_code => service_code,
|
161
|
+
:total_price => product.get_text('rate').to_s,
|
162
|
+
:currency => 'CAD',
|
163
|
+
:delivery_range => [product.get_text('deliveryDate').to_s] * 2
|
164
|
+
)
|
165
|
+
end
|
166
|
+
|
167
|
+
boxes = xml.elements.collect('eparcel/ratesAndServicesResponse/packing/box') do |box|
|
168
|
+
b = Box.new
|
169
|
+
b.packedItems = []
|
170
|
+
b.name = box.get_text('name').to_s
|
171
|
+
b.weight = box.get_text('weight').to_s.to_f
|
172
|
+
b.expediter_weight = box.get_text('expediterWeight').to_s.to_f
|
173
|
+
b.length = box.get_text('length').to_s.to_f
|
174
|
+
b.width = box.get_text('width').to_s.to_f
|
175
|
+
b.height = box.get_text('height').to_s.to_f
|
176
|
+
b.packedItems = box.elements.collect('packedItem') do |item|
|
177
|
+
p = PackedItem.new
|
178
|
+
p.quantity = item.get_text('quantity').to_s.to_i
|
179
|
+
p.description = item.get_text('description').to_s
|
180
|
+
p
|
181
|
+
end
|
182
|
+
b
|
183
|
+
end
|
184
|
+
|
185
|
+
postal_outlets = xml.elements.collect('eparcel/ratesAndServicesResponse/nearestPostalOutlet') do |outlet|
|
186
|
+
postal_outlet = PostalOutlet.new
|
187
|
+
postal_outlet.sequence_no = outlet.get_text('postalOutletSequenceNo').to_s
|
188
|
+
postal_outlet.distance = outlet.get_text('distance').to_s
|
189
|
+
postal_outlet.name = outlet.get_text('outletName').to_s
|
190
|
+
postal_outlet.business_name = outlet.get_text('businessName').to_s
|
191
|
+
|
192
|
+
postal_outlet.postal_address = Location.new({
|
193
|
+
:address1 => outlet.get_text('postalAddress/addressLine').to_s,
|
194
|
+
:postal_code => outlet.get_text('postalAddress/postal_code').to_s,
|
195
|
+
:city => outlet.get_text('postalAddress/municipality').to_s,
|
196
|
+
:province => outlet.get_text('postalAddress/province').to_s,
|
197
|
+
:country => 'Canada',
|
198
|
+
:phone_number => outlet.get_text('phoneNumber').to_s
|
199
|
+
})
|
200
|
+
|
201
|
+
postal_outlet.business_hours = outlet.elements.collect('businessHours') do |hour|
|
202
|
+
{ :day_of_week => hour.get_text('dayOfWeek').to_s, :time => hour.get_text('time').to_s }
|
203
|
+
end
|
204
|
+
|
205
|
+
postal_outlet
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
CanadaPostRateResponse.new(success, message, Hash.from_xml(response), :rates => rate_estimates, :xml => response, :boxes => boxes, :postal_outlets => postal_outlets)
|
210
|
+
end
|
211
|
+
|
212
|
+
def response_success?(xml)
|
213
|
+
value = xml.get_text('eparcel/ratesAndServicesResponse/statusCode').to_s
|
214
|
+
value == '1' || value == '2'
|
215
|
+
end
|
216
|
+
|
217
|
+
def response_message(xml)
|
218
|
+
xml.get_text('eparcel/ratesAndServicesResponse/statusMessage').to_s
|
219
|
+
end
|
220
|
+
|
221
|
+
# <!-- List of items in the shopping -->
|
222
|
+
# <!-- cart -->
|
223
|
+
# <!-- Each item is defined by : -->
|
224
|
+
# <!-- - quantity (mandatory) -->
|
225
|
+
# <!-- - size (mandatory) -->
|
226
|
+
# <!-- - weight (mandatory) -->
|
227
|
+
# <!-- - description (mandatory) -->
|
228
|
+
# <!-- - ready to ship (optional) -->
|
229
|
+
|
230
|
+
def build_line_items(line_items)
|
231
|
+
xml_line_items = XmlNode.new('lineItems') do |line_items_node|
|
232
|
+
|
233
|
+
line_items.each do |line_item|
|
234
|
+
|
235
|
+
line_items_node << XmlNode.new('item') do |item|
|
236
|
+
item << XmlNode.new('quantity', 1)
|
237
|
+
item << XmlNode.new('weight', line_item.kilograms)
|
238
|
+
item << XmlNode.new('length', line_item.cm(:length).to_s)
|
239
|
+
item << XmlNode.new('width', line_item.cm(:width).to_s)
|
240
|
+
item << XmlNode.new('height', line_item.cm(:height).to_s)
|
241
|
+
item << XmlNode.new('description', line_item.options[:description] || ' ')
|
242
|
+
item << XmlNode.new('readyToShip', line_item.options[:ready_to_ship] || nil)
|
243
|
+
|
244
|
+
# By setting the 'readyToShip' tag to true, Sell Online will not pack this item in the boxes defined in the merchant profile.
|
245
|
+
end
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
xml_line_items
|
250
|
+
end
|
251
|
+
|
252
|
+
def dollar_amount(cents)
|
253
|
+
"%0.2f" % (cents / 100.0)
|
254
|
+
end
|
255
|
+
|
256
|
+
def handle_non_iso_country_names(country)
|
257
|
+
NON_ISO_COUNTRY_NAMES[country.to_s] || country
|
258
|
+
end
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|