Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Merge branch 'canada_post_pws'

Conflicts:
	.gitignore
  • Loading branch information...
commit 2323fece62e795cffa7a588e689668d79dedb86c 2 parents af68a1c + a1c38cf
@jnormore jnormore authored
Showing with 3,104 additions and 19 deletions.
  1. +3 −0  .gitignore
  2. +1 −0  active_shipping.gemspec
  3. +1 −0  lib/active_shipping.rb
  4. +2 −1  lib/active_shipping/shipping/carriers.rb
  5. +4 −4 lib/active_shipping/shipping/carriers/canada_post.rb
  6. +828 −0 lib/active_shipping/shipping/carriers/canada_post_pws.rb
  7. +112 −9 lib/active_shipping/shipping/package.rb
  8. +16 −0 lib/active_shipping/shipping/shipping_response.rb
  9. +5 −1 test/fixtures.yml
  10. BIN  test/fixtures/files/label1.pdf
  11. +112 −0 test/fixtures/xml/canadapost_pws/dnc_tracking_details_en.xml
  12. +7 −0 test/fixtures/xml/canadapost_pws/merchant_details_error.xml
  13. +7 −0 test/fixtures/xml/canadapost_pws/merchant_details_response.xml
  14. +13 −0 test/fixtures/xml/canadapost_pws/option_response.xml
  15. +7 −0 test/fixtures/xml/canadapost_pws/option_response_no_conflicts.xml
  16. +190 −0 test/fixtures/xml/canadapost_pws/rates_info.xml
  17. +7 −0 test/fixtures/xml/canadapost_pws/rates_info_error.xml
  18. +42 −0 test/fixtures/xml/canadapost_pws/receipt_response.xml
  19. +36 −0 test/fixtures/xml/canadapost_pws/receipt_response_no_priced_options.xml
  20. +7 −0 test/fixtures/xml/canadapost_pws/register_token_error.xml
  21. +3 −0  test/fixtures/xml/canadapost_pws/register_token_response.xml
  22. +42 −0 test/fixtures/xml/canadapost_pws/service_options_response.xml
  23. +6 −0 test/fixtures/xml/canadapost_pws/services_error.xml
  24. +32 −0 test/fixtures/xml/canadapost_pws/services_response.xml
  25. +69 −0 test/fixtures/xml/canadapost_pws/shipment_domestic.xml
  26. +20 −0 test/fixtures/xml/canadapost_pws/shipment_response.xml
  27. +69 −0 test/fixtures/xml/canadapost_pws/shipment_us.xml
  28. +152 −0 test/fixtures/xml/canadapost_pws/tracking_details_en.xml
  29. +7 −0 test/fixtures/xml/canadapost_pws/tracking_details_en_error.xml
  30. +156 −0 test/fixtures/xml/canadapost_pws/tracking_details_fr.xml
  31. +247 −0 test/remote/canada_post_pws_platform_test.rb
  32. +162 −0 test/remote/canada_post_pws_test.rb
  33. +11 −4 test/test_helper.rb
  34. +294 −0 test/unit/carriers/canada_post_pws_rating_test.rb
  35. +74 −0 test/unit/carriers/canada_post_pws_register_test.rb
  36. +239 −0 test/unit/carriers/canada_post_pws_shipping_test.rb
  37. +121 −0 test/unit/carriers/canada_post_pws_tracking_test.rb
View
3  .gitignore
@@ -6,3 +6,6 @@ pkg
.dotest
Gemfile.lock
.rvmrc
+test/fixtures/live_credentials.yml
+.rbenv-version
+
View
1  active_shipping.gemspec
@@ -25,6 +25,7 @@ Gem::Specification.new do |s|
s.add_development_dependency('rake')
s.add_development_dependency('mocha')
s.add_development_dependency('timecop')
+ s.add_development_dependency('nokogiri')
s.files = Dir.glob("lib/**/*") + %w(MIT-LICENSE README.markdown CHANGELOG)
s.require_path = 'lib'
View
1  lib/active_shipping.rb
@@ -41,6 +41,7 @@
require 'active_shipping/shipping/response'
require 'active_shipping/shipping/rate_response'
require 'active_shipping/shipping/tracking_response'
+require 'active_shipping/shipping/shipping_response'
require 'active_shipping/shipping/package'
require 'active_shipping/shipping/location'
require 'active_shipping/shipping/rate_estimate'
View
3  lib/active_shipping/shipping/carriers.rb
@@ -6,13 +6,14 @@
require 'active_shipping/shipping/carriers/kunaki'
require 'active_shipping/shipping/carriers/canada_post'
require 'active_shipping/shipping/carriers/new_zealand_post'
+require 'active_shipping/shipping/carriers/canada_post_pws'
module ActiveMerchant
module Shipping
module Carriers
class <<self
def all
- [BogusCarrier, UPS, USPS, FedEx, Shipwire, Kunaki, CanadaPost, NewZealandPost]
+ [BogusCarrier, UPS, USPS, FedEx, Shipwire, Kunaki, CanadaPost, NewZealandPost, CanadaPostPWS]
end
end
end
View
8 lib/active_shipping/shipping/carriers/canada_post.rb
@@ -99,10 +99,10 @@ def maximum_weight
def self.default_location
{
- :country => 'CA',
- :province => 'ON',
- :city => 'Ottawa',
- :address1 => '61A York St',
+ :country => 'CA',
+ :province => 'ON',
+ :city => 'Ottawa',
+ :address1 => '61A York St',
:postal_code => 'K1N5T2'
}
end
View
828 lib/active_shipping/shipping/carriers/canada_post_pws.rb
@@ -0,0 +1,828 @@
+require 'cgi'
+
+module ActiveMerchant
+ module Shipping
+
+ class CanadaPostPWS < Carrier
+ @@name = "Canada Post PWS"
+
+ SHIPPING_SERVICES = {
+ "DOM.RP" => "Regular Parcel",
+ "DOM.EP" => "Expedited Parcel",
+ "DOM.XP" => "Xpresspost",
+ "DOM.XP.CERT" => "Xpresspost Certified",
+ "DOM.PC" => "Priority",
+ "DOM.LIB" => "Library Books",
+
+ "USA.EP" => "Expedited Parcel USA",
+ "USA.PW.ENV" => "Priority Worldwide Envelope USA",
+ "USA.PW.PAK" => "Priority Worldwide pak USA",
+ "USA.PW.PARCEL" => "Priority Worldwide Parcel USA",
+ "USA.SP.AIR" => "Small Packet USA Air",
+ "USA.SP.SURF" => "Small Packet USA Surface",
+ "USA.XP" => "Xpresspost USA",
+
+ "INT.XP" => "Xpresspost International",
+ "INT.IP.AIR" => "International Parcel Air",
+ "INT.IP.SURF" => "International Parcel Surface",
+ "INT.PW.ENV" => "Priority Worldwide Envelope Int'l",
+ "INT.PW.PAK" => "Priority Worldwide pak Int'l",
+ "INT.PW.PARCEL" => "Priority Worldwide parcel Int'l",
+ "INT.SP.AIR" => "Small Packet International Air",
+ "INT.SP.SURF" => "Small Packet International Surface"
+ }
+
+ ENDPOINT = "https://soa-gw.canadapost.ca/" # production
+
+ SHIPMENT_MIMETYPE = "application/vnd.cpc.ncshipment+xml"
+ RATE_MIMETYPE = "application/vnd.cpc.ship.rate+xml"
+ TRACK_MIMETYPE = "application/vnd.cpc.track+xml"
+ REGISTER_MIMETYPE = "application/vnd.cpc.registration+xml"
+
+ LANGUAGE = {
+ 'en' => 'en-CA',
+ 'fr' => 'fr-CA'
+ }
+
+ SHIPPING_OPTIONS = [:d2po, :d2po_office_id, :cov, :cov_amount, :cod, :cod_amount, :cod_includes_shipping,
+ :cod_method_of_payment, :so, :dc, :dns, :pa18, :pa19, :hfp, :lad,
+ :rase, :rts, :aban]
+
+ RATES_OPTIONS = [:cov, :cov_amount, :cod, :so, :dc, :dns, :pa18, :pa19, :hfp, :lad]
+
+ MAX_WEIGHT = 30 # kg
+
+ attr_accessor :language, :endpoint, :logger, :platform_id
+
+ def initialize(options = {})
+ @language = LANGUAGE[options[:language]] || LANGUAGE['en']
+ @endpoint = options[:endpoint] || ENDPOINT
+ @platform_id = options[:platform_id]
+ super(options)
+ end
+
+ def requirements
+ [:api_key, :secret]
+ end
+
+ def find_rates(origin, destination, line_items = [], options = {}, package = nil, services = [])
+ url = endpoint + "rs/ship/price"
+ request = build_rates_request(origin, destination, line_items, options, package, services)
+ response = ssl_post(url, request, headers(options, RATE_MIMETYPE, RATE_MIMETYPE))
+ parse_rates_response(response, origin, destination)
+ rescue ActiveMerchant::ResponseError, ActiveMerchant::Shipping::ResponseError => e
+ error_response(e.response.body, CPPWSRateResponse)
+ end
+
+ def find_tracking_info(pin, options = {})
+ response = ssl_get(tracking_url(pin), headers(options, TRACK_MIMETYPE))
+ parse_tracking_response(response)
+ rescue ActiveMerchant::ResponseError, ActiveMerchant::Shipping::ResponseError => e
+ error_response(e.response.body, CPPWSTrackingResponse)
+ rescue InvalidPinFormatError => e
+ CPPWSTrackingResponse.new(false, "Invalid Pin Format", {}, {:carrier => @@name})
+ end
+
+ # line_items should be a list of PackageItem's
+ def create_shipment(origin, destination, package, line_items = [], options = {})
+ request_body = build_shipment_request(origin, destination, package, line_items, options)
+ response = ssl_post(create_shipment_url(options), request_body, headers(options, SHIPMENT_MIMETYPE, SHIPMENT_MIMETYPE))
+ parse_shipment_response(response)
+ rescue ActiveMerchant::ResponseError, ActiveMerchant::Shipping::ResponseError => e
+ error_response(e.response.body, CPPWSShippingResponse)
+ rescue MissingCustomerNumberError => e
+ CPPWSShippingResponse.new(false, "Missing Customer Number", {}, {:carrier => @@name})
+ end
+
+ def retrieve_shipment(shipping_id, options = {})
+ response = ssl_post(shipment_url(shipping_id, options), nil, headers(options, SHIPMENT_MIMETYPE, SHIPMENT_MIMETYPE))
+ shipping_response = parse_shipment_response(response)
+ end
+
+ def find_shipment_receipt(shipping_id, options = {})
+ response = ssl_get(shipment_receipt_url(shipping_id, options), headers(options, SHIPMENT_MIMETYPE, SHIPMENT_MIMETYPE))
+ shipping_response = parse_shipment_receipt_response(response)
+ end
+
+ def retrieve_shipping_label(shipping_response, options = {})
+ raise MissingShippingNumberError unless shipping_response && shipping_response.shipping_id
+ ssl_get(shipping_response.label_url, headers(options, "application/pdf"))
+ end
+
+ def register_merchant(options = {})
+ url = endpoint + "ot/token"
+ response = ssl_post(url, nil, headers({}, REGISTER_MIMETYPE, REGISTER_MIMETYPE).merge({"Content-Length" => "0"}))
+ parse_register_token_response(response)
+ rescue ActiveMerchant::ResponseError, ActiveMerchant::Shipping::ResponseError => e
+ error_response(e.response.body, CPPWSRegisterResponse)
+ end
+
+ def retrieve_merchant_details(options = {})
+ raise MissingTokenIdError unless token_id = options[:token_id]
+ url = endpoint + "ot/token/#{token_id}"
+ response = ssl_get(url, headers({}, REGISTER_MIMETYPE, REGISTER_MIMETYPE))
+ parse_merchant_details_response(response)
+ rescue ActiveMerchant::ResponseError, ActiveMerchant::Shipping::ResponseError => e
+ error_response(e.response.body, CPPWSMerchantDetailsResponse)
+ rescue Exception => e
+ raise ResponseError.new(e.message)
+ end
+
+ def find_services(country = nil, options = {})
+ response = ssl_get(services_url(country), headers(options, RATE_MIMETYPE))
+ parse_services_response(response)
+ rescue ActiveMerchant::ResponseError, ActiveMerchant::Shipping::ResponseError => e
+ error_response(e.response.body, CPPWSRateResponse)
+ end
+
+ def find_service_options(service_code, country, options = {})
+ response = ssl_get(services_url(country, service_code), headers(options, RATE_MIMETYPE))
+ parse_service_options_response(response)
+ rescue ActiveMerchant::ResponseError, ActiveMerchant::Shipping::ResponseError => e
+ error_response(e.response.body, CPPWSRateResponse)
+ end
+
+ def find_option_details(option_code, options = {})
+ url = endpoint + "rs/ship/option/#{option_code}"
+ response = ssl_get(url, headers(options, RATE_MIMETYPE))
+ parse_option_response(response)
+ rescue ActiveMerchant::ResponseError, ActiveMerchant::Shipping::ResponseError => e
+ error_response(e.response.body, CPPWSRateResponse)
+ end
+
+ def maximum_weight
+ Mass.new(MAX_WEIGHT, :kilograms)
+ end
+
+ # service discovery
+
+ def parse_services_response(response)
+ doc = REXML::Document.new(REXML::Text::unnormalize(response))
+ service_nodes = doc.elements['services'].elements.collect('service') {|node| node }
+ services = service_nodes.inject({}) do |result, node|
+ service_code = node.get_text("service-code").to_s
+ service_name = node.get_text("service-name").to_s
+ service_link = node.elements["link"].attributes['href']
+ service_link_media_type = node.elements["link"].attributes['media-type']
+ result[service_code] = {
+ :name => service_name,
+ :link => service_link,
+ :link_media_type => service_link_media_type
+ }
+ result
+ end
+ services
+ end
+
+ def parse_service_options_response(response)
+ doc = REXML::Document.new(REXML::Text::unnormalize(response))
+ service_node = doc.elements['service']
+ service_code = service_node.get_text("service-code").to_s
+ service_name = service_node.get_text("service-name").to_s
+ options_node = service_node.elements['options']
+ unless options_node.blank?
+ option_nodes = options_node.elements.collect('option') {|node| node}
+ options = option_nodes.inject([]) do |result, node|
+ option = {
+ :code => node.get_text("option-code").to_s,
+ :name => node.get_text("option-name").to_s,
+ :required => node.get_text("mandatory").to_s == "false" ? false : true,
+ :qualifier_required => node.get_text("qualifier-required").to_s == "false" ? false : true
+ }
+ option[:qualifier_max] = node.get_text("qualifier-max").to_s.to_i if node.get_text("qualifier-max")
+ result << option
+ result
+ end
+ end
+ restrictions_node = service_node.elements['restrictions']
+ dimensions_node = restrictions_node.elements['dimensional-restrictions']
+ restrictions = {
+ :min_weight => restrictions_node.elements["weight-restriction"].attributes['min'].to_i,
+ :max_weight => restrictions_node.elements["weight-restriction"].attributes['max'].to_i,
+ :min_length => dimensions_node.elements["length"].attributes['min'].to_f,
+ :max_length => dimensions_node.elements["length"].attributes['max'].to_f,
+ :min_height => dimensions_node.elements["height"].attributes['min'].to_f,
+ :max_height => dimensions_node.elements["height"].attributes['max'].to_f,
+ :min_width => dimensions_node.elements["width"].attributes['min'].to_f,
+ :max_width => dimensions_node.elements["width"].attributes['max'].to_f,
+ }
+
+ {
+ :service_code => service_code,
+ :service_name => service_name,
+ :options => options,
+ :restrictions => restrictions
+ }
+ end
+
+ def parse_option_response(response)
+ doc = REXML::Document.new(REXML::Text::unnormalize(response))
+ option_node = doc.elements['option']
+ conflicts = option_node.elements['conflicting-options'].elements.collect('option-code') {|node| node.get_text.to_s} unless option_node.elements['conflicting-options'].blank?
+ prereqs = option_node.elements['prerequisite-options'].elements.collect('option-code') {|node| node.get_text.to_s} unless option_node.elements['prerequisite-options'].blank?
+ option = {
+ :code => option_node.get_text('option-code').to_s,
+ :name => option_node.get_text('option-name').to_s,
+ :class => option_node.get_text('option-class').to_s,
+ :prints_on_label => option_node.get_text('prints-on-label').to_s == "false" ? false : true,
+ :qualifier_required => option_node.get_text('qualifier-required').to_s == "false" ? false : true
+ }
+ option[:conflicting_options] = conflicts if conflicts
+ option[:prerequisite_options] = prereqs if prereqs
+
+ option[:qualifier_max] = option_node.get_text("qualifier-max").to_s.to_i if option_node.get_text("qualifier-max")
+ option
+ end
+
+ # rating
+
+ def build_rates_request(origin, destination, line_items = [], options = {}, package = nil, services = [])
+ xml = XmlNode.new('mailing-scenario', :xmlns => "http://www.canadapost.ca/ws/ship/rate") do |node|
+ node << customer_number_node(options)
+ node << contract_id_node(options)
+ node << quote_type_node(options)
+ options_node = shipping_options_node(RATES_OPTIONS, options)
+ node << options_node if options_node && !options_node.children.count.zero?
+ node << parcel_node(line_items, package)
+ node << origin_node(origin)
+ node << destination_node(destination)
+ node << services_node(services) unless services.blank?
+ end
+ xml.to_s
+ end
+
+ def parse_rates_response(response, origin, destination)
+ doc = REXML::Document.new(REXML::Text::unnormalize(response))
+ raise ActiveMerchant::Shipping::ResponseError, "No Quotes" unless doc.elements['price-quotes']
+
+ quotes = doc.elements['price-quotes'].elements.collect('price-quote') {|node| node }
+ rates = quotes.map do |node|
+ service_name = node.get_text("service-name").to_s
+ service_code = node.get_text("service-code").to_s
+ total_price = node.elements['price-details'].get_text("due").to_s
+ expected_date = expected_date_from_node(node)
+ options = {
+ :service_code => service_code,
+ :total_price => total_price,
+ :currency => 'CAD',
+ :delivery_range => [expected_date, expected_date]
+ }
+ RateEstimate.new(origin, destination, @@name, service_name, options)
+ end
+ CPPWSRateResponse.new(true, "", {}, :rates => rates)
+ end
+
+
+ # tracking
+
+ def parse_tracking_response(response)
+ doc = REXML::Document.new(REXML::Text::unnormalize(response))
+ raise ActiveMerchant::Shipping::ResponseError, "No Tracking" unless root_node = doc.elements['tracking-detail']
+
+ events = root_node.elements['significant-events'].elements.collect('occurrence') {|node| node }
+
+ shipment_events = build_tracking_events(events)
+ change_date = root_node.get_text('changed-expected-date').to_s
+ expected_date = root_node.get_text('expected-delivery-date').to_s
+ dest_postal_code = root_node.get_text('destination-postal-id').to_s
+ destination = Location.new(:postal_code => dest_postal_code)
+ origin = Location.new({})
+ options = {
+ :carrier => @@name,
+ :service_name => root_node.get_text('service-name').to_s,
+ :expected_date => Date.parse(expected_date),
+ :changed_date => change_date.blank? ? nil : Date.parse(change_date),
+ :change_reason => root_node.get_text('changed-expected-delivery-reason').to_s.strip,
+ :destination_postal_code => root_node.get_text('destination-postal-id').to_s,
+ :shipment_events => shipment_events,
+ :tracking_number => root_node.get_text('pin').to_s,
+ :origin => origin,
+ :destination => destination,
+ :customer_number => root_node.get_text('mailed-by-customer-number').to_s
+ }
+
+ CPPWSTrackingResponse.new(true, "", {}, options)
+ end
+
+ def build_tracking_events(events)
+ events.map do |event|
+ date = event.get_text('event-date').to_s
+ time = event.get_text('event-time').to_s
+ zone = event.get_text('event-time-zone').to_s
+ timestamp = DateTime.parse("#{date} #{time} #{zone}")
+ time = Time.utc(timestamp.utc.year, timestamp.utc.month, timestamp.utc.day, timestamp.utc.hour, timestamp.utc.min, timestamp.utc.sec)
+ message = event.get_text('event-description').to_s
+ location = [event.get_text('event-retail-name'), event.get_text('event-site'), event.get_text('event-province')].compact.join(", ")
+ name = event.get_text('event-identifier').to_s
+ ShipmentEvent.new(name, time, location, message)
+ end
+ end
+
+
+ # shipping
+
+ # options
+ # :service => 'DOM.EP'
+ # :notification_email
+ # :packing_instructions
+ # :show_postage_rate
+ # :cod, :cod_amount, :insurance, :insurance_amount, :signature_required, :pa18, :pa19, :hfp, :dns, :lad
+ #
+ def build_shipment_request(origin_hash, destination_hash, package, line_items = [], options = {})
+ origin = Location.new(sanitize_zip(origin_hash))
+ destination = Location.new(sanitize_zip(destination_hash))
+
+ xml = XmlNode.new('non-contract-shipment', :xmlns => "http://www.canadapost.ca/ws/ncshipment") do |root_node|
+ root_node << XmlNode.new('delivery-spec') do |node|
+ node << shipment_service_code_node(options)
+ node << shipment_sender_node(origin, options)
+ node << shipment_destination_node(destination, options)
+ options_node = shipment_options_node(options)
+ node << shipment_options_node(options) if options_node && !options_node.children.count.zero?
+ node << shipment_parcel_node(package)
+ node << shipment_notification_node(options)
+ node << shipment_preferences_node(options)
+ node << references_node(options) # optional > user defined custom notes
+ node << shipment_customs_node(destination, line_items, options)
+ # COD Remittance defaults to sender
+ end
+ end
+ xml.to_s
+ end
+
+ def shipment_service_code_node(options)
+ XmlNode.new('service-code', options[:service])
+ end
+
+ def shipment_sender_node(location, options)
+ XmlNode.new('sender') do |node|
+ node << XmlNode.new('name', location.name)
+ node << XmlNode.new('company', location.company) if location.company.present?
+ node << XmlNode.new('contact-phone', location.phone)
+ node << XmlNode.new('address-details') do |innernode|
+ innernode << XmlNode.new('address-line-1', location.address1)
+ address2 = [location.address2, location.address3].reject(&:blank?).join(", ")
+ innernode << XmlNode.new('address-line-2', address2) unless address2.blank?
+ innernode << XmlNode.new('city', location.city)
+ innernode << XmlNode.new('prov-state', location.province)
+ #innernode << XmlNode.new('country-code', location.country_code)
+ innernode << XmlNode.new('postal-zip-code', location.postal_code)
+ end
+ end
+ end
+
+ def shipment_destination_node(location, options)
+ XmlNode.new('destination') do |node|
+ node << XmlNode.new('name', location.name)
+ node << XmlNode.new('company', location.company) if location.company.present?
+ node << XmlNode.new('client-voice-number', location.phone)
+ node << XmlNode.new('address-details') do |innernode|
+ innernode << XmlNode.new('address-line-1', location.address1)
+ address2 = [location.address2, location.address3].reject(&:blank?).join(", ")
+ innernode << XmlNode.new('address-line-2', address2) unless address2.blank?
+ innernode << XmlNode.new('city', location.city)
+ innernode << XmlNode.new('prov-state', location.province) unless location.province.blank?
+ innernode << XmlNode.new('country-code', location.country_code)
+ innernode << XmlNode.new('postal-zip-code', location.postal_code)
+ end
+ end
+ end
+
+ def shipment_options_node(options)
+ shipping_options_node(SHIPPING_OPTIONS, options)
+ end
+
+ def shipment_notification_node(options)
+ return unless options[:notification_email]
+ XmlNode.new('notification') do |node|
+ node << XmlNode.new('email', options[:notification_email])
+ node << XmlNode.new('on-shipment', true)
+ node << XmlNode.new('on-exception', true)
+ node << XmlNode.new('on-delivery', true)
+ end
+ end
+
+ def shipment_preferences_node(options)
+ XmlNode.new('preferences') do |node|
+ node << XmlNode.new('show-packing-instructions', options[:packing_instructions] || true)
+ node << XmlNode.new('show-postage-rate', options[:show_postage_rate] || false)
+ node << XmlNode.new('show-insured-value', true)
+ end
+ end
+
+ def references_node(options)
+ # custom values
+ # XmlNode.new('references') do |node|
+ # end
+ end
+
+ def shipment_customs_node(destination, line_items, options)
+ return unless destination.country_code != 'CA'
+
+ XmlNode.new('customs') do |node|
+ currency = options[:currency] || "CAD"
+ node << XmlNode.new('currency',currency)
+ node << XmlNode.new('conversion-from-cad',options[:conversion_from_cad].to_s) if currency != 'CAD' && options[:conversion_from_cad]
+ node << XmlNode.new('reason-for-export','SOG') # SOG - Sale of Goods
+ node << XmlNode.new('other-reason',options[:customs_other_reason]) if (options[:customs_reason_for_export] && options[:customs_other_reason])
+ node << XmlNode.new('additional-customs-info',options[:customs_addition_info]) if options[:customs_addition_info]
+ node << XmlNode.new('sku-list') do |sku|
+ line_items.each do |line_item|
+ sku << XmlNode.new('item') do |item|
+ item << XmlNode.new('hs-tariff-code', line_item.hs_code) if line_item.hs_code && !line_item.hs_code.empty?
+ item << XmlNode.new('sku', line_item.sku) if line_item.sku && !line_item.sku.empty?
+ item << XmlNode.new('customs-description', line_item.name.slice(0,44))
+ item << XmlNode.new('unit-weight', '%#2.3f' % sanitize_weight_kg(line_item.kg))
+ item << XmlNode.new('customs-value-per-unit', '%.2f' % sanitize_price_from_cents(line_item.value_per_unit))
+ item << XmlNode.new('customs-number-of-units', line_item.quantity)
+ 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?
+ 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?
+ end
+ end
+ end
+
+ end
+ end
+
+ def shipment_parcel_node(package, options ={})
+ weight = sanitize_weight_kg(package.kilograms.to_f)
+ XmlNode.new('parcel-characteristics') do |el|
+ el << XmlNode.new('weight', "%#2.3f" % weight)
+ pkg_dim = package.cm
+ if pkg_dim && !pkg_dim.select{|x| x != 0}.empty?
+ el << XmlNode.new('dimensions') do |dim|
+ dim << XmlNode.new('length', '%.1f' % ((pkg_dim[2]*10).round / 10.0)) if pkg_dim.size >= 3
+ dim << XmlNode.new('width', '%.1f' % ((pkg_dim[1]*10).round / 10.0)) if pkg_dim.size >= 2
+ dim << XmlNode.new('height', '%.1f' % ((pkg_dim[0]*10).round / 10.0)) if pkg_dim.size >= 1
+ end
+ end
+ el << XmlNode.new('document', false)
+ el << XmlNode.new('mailing-tube', package.tube?)
+ el << XmlNode.new('unpackaged', package.unpackaged?)
+ end
+ end
+
+
+ def parse_shipment_response(response)
+ doc = REXML::Document.new(REXML::Text::unnormalize(response))
+ raise ActiveMerchant::Shipping::ResponseError, "No Shipping" unless root_node = doc.elements['non-contract-shipment-info']
+ options = {
+ :shipping_id => root_node.get_text('shipment-id').to_s,
+ :tracking_number => root_node.get_text('tracking-pin').to_s,
+ :details_url => root_node.elements["links/link[@rel='details']"].attributes['href'],
+ :label_url => root_node.elements["links/link[@rel='label']"].attributes['href'],
+ :receipt_url => root_node.elements["links/link[@rel='receipt']"].attributes['href']
+ }
+ CPPWSShippingResponse.new(true, "", {}, options)
+ end
+
+ def parse_register_token_response(response)
+ doc = REXML::Document.new(REXML::Text::unnormalize(response))
+ raise ActiveMerchant::Shipping::ResponseError, "No Registration Token" unless root_node = doc.elements['token']
+ options = {
+ :token_id => root_node.get_text('token-id').to_s
+ }
+ CPPWSRegisterResponse.new(true, "", {}, options)
+ end
+
+ def parse_merchant_details_response(response)
+ doc = REXML::Document.new(REXML::Text::unnormalize(response))
+ raise "No Merchant Info" unless root_node = doc.elements['merchant-info']
+ raise "No Merchant Info" if root_node.get_text('customer-number').blank?
+ options = {
+ :customer_number => root_node.get_text('customer-number').to_s,
+ :contract_number => root_node.get_text('contract-number').to_s,
+ :username => root_node.get_text('merchant-username').to_s,
+ :password => root_node.get_text('merchant-password').to_s,
+ :has_default_credit_card => root_node.get_text('has-default-credit-card') == 'true'
+ }
+ CPPWSMerchantDetailsResponse.new(true, "", {}, options)
+ end
+
+ def parse_shipment_receipt_response(response)
+ doc = REXML::Document.new(REXML::Text::unnormalize(response))
+ root = doc.elements['non-contract-shipment-receipt']
+ cc_details_node = root.elements['cc-receipt-details']
+ service_standard_node = root.elements['service-standard']
+ receipt = {
+ :final_shipping_point => root.get_text("final-shipping-point").to_s,
+ :shipping_point_name => root.get_text("shipping-point-name").to_s,
+ :service_code => root.get_text("service-code").to_s,
+ :rated_weight => root.get_text("rated-weight").to_s.to_f,
+ :base_amount => root.get_text("base-amount").to_s.to_f,
+ :pre_tax_amount => root.get_text("pre-tax-amount").to_s.to_f,
+ :gst_amount => root.get_text("gst-amount").to_s.to_f,
+ :pst_amount => root.get_text("pst-amount").to_s.to_f,
+ :hst_amount => root.get_text("hst-amount").to_s.to_f,
+ :charge_amount => cc_details_node.get_text("charge-amount").to_s.to_f,
+ :currency => cc_details_node.get_text("currency").to_s,
+ :expected_transit_days => service_standard_node.get_text("expected-transit-time").to_s.to_i,
+ :expected_delivery_date => service_standard_node.get_text("expected-delivery-date").to_s
+ }
+ option_nodes = root.elements['priced-options'].elements.collect('priced-option') {|node| node} unless root.elements['priced-options'].blank?
+
+ receipt[:priced_options] = if option_nodes
+ option_nodes.inject({}) do |result, node|
+ result[node.get_text("option-code").to_s] = node.get_text("option-price").to_s.to_f
+ result
+ end
+ else
+ []
+ end
+
+ else
+
+ receipt
+ end
+
+ def error_response(response, response_klass)
+ doc = REXML::Document.new(REXML::Text::unnormalize(response))
+ messages = doc.elements['messages'].elements.collect('message') {|node| node }
+ message = messages.map {|m| m.get_text('description').to_s }.join(", ")
+ code = messages.map {|m| m.get_text('code').to_s }.join(", ")
+ response_klass.new(false, message, {}, {:carrier => @@name, :code => code})
+ end
+
+ def log(msg)
+ logger.debug(msg) if logger
+ end
+
+ private
+
+ def tracking_url(pin)
+ case pin.length
+ when 12,13,16
+ endpoint + "vis/track/pin/%s/detail" % pin
+ when 15
+ endpoint + "vis/track/dnc/%s/detail" % pin
+ else
+ raise InvalidPinFormatError
+ end
+ end
+
+ def create_shipment_url(options)
+ raise MissingCustomerNumberError unless customer_number = options[:customer_number]
+ if @platform_id.present?
+ endpoint + "rs/#{customer_number}-#{@platform_id}/ncshipment"
+ else
+ endpoint + "rs/#{customer_number}/ncshipment"
+ end
+ end
+
+ def shipment_url(shipping_id, options={})
+ raise MissingCustomerNumberError unless customer_number = options[:customer_number]
+ if @platform_id.present?
+ endpoint + "rs/#{customer_number}-#{@platform_id}/ncshipment/#{shipping_id}"
+ else
+ endpoint + "rs/#{customer_number}/ncshipment/#{shipping_id}"
+ end
+ end
+
+ def shipment_receipt_url(shipping_id, options={})
+ raise MissingCustomerNumberError unless customer_number = options[:customer_number]
+ if @platform_id.present?
+ endpoint + "rs/#{customer_number}-#{@platform_id}/ncshipment/#{shipping_id}/receipt"
+ else
+ endpoint + "rs/#{customer_number}/ncshipment/#{shipping_id}/receipt"
+ end
+ end
+
+ def services_url(country=nil, service_code=nil)
+ url = endpoint + "rs/ship/service"
+ url += "/#{service_code}" if service_code
+ url += "?country=#{country}" if country
+ end
+
+ def customer_credentials_valid?(credentials)
+ (credentials.keys & [:customer_api_key, :customer_secret]).any?
+ end
+
+ def encoded_authorization(customer_credentials = {})
+ if customer_credentials_valid?(customer_credentials)
+ "Basic %s" % ActiveSupport::Base64.encode64("#{customer_credentials[:customer_api_key]}:#{customer_credentials[:customer_secret]}")
+ else
+ "Basic %s" % ActiveSupport::Base64.encode64("#{@options[:api_key]}:#{@options[:secret]}")
+ end
+ end
+
+ def headers(customer_credentials, accept = nil, content_type = nil)
+ headers = {
+ 'Authorization' => encoded_authorization(customer_credentials),
+ 'Accept-Language' => language
+ }
+ headers['Accept'] = accept if accept
+ headers['Content-Type'] = content_type if content_type
+ headers['Platform-ID'] = platform_id if platform_id && customer_credentials_valid?(customer_credentials)
+ headers
+ end
+
+ def customer_number_node(options)
+ XmlNode.new("customer-number", options[:customer_number])
+ end
+
+ def contract_id_node(options)
+ XmlNode.new("contract-id", options[:contract_id]) if options[:contract_id]
+ end
+
+ def quote_type_node(options)
+ XmlNode.new("quote-type", 'commercial')
+ end
+
+ def parcel_node(line_items, package = nil, options ={})
+ weight = sanitize_weight_kg(package && !package.kilograms.zero? ? package.kilograms.to_f : line_items.sum(&:kilograms).to_f)
+ XmlNode.new('parcel-characteristics') do |el|
+ el << XmlNode.new('weight', "%#2.3f" % weight)
+ if package
+ pkg_dim = package.cm
+ if pkg_dim && !pkg_dim.select{|x| x != 0}.empty?
+ el << XmlNode.new('dimensions') do |dim|
+ dim << XmlNode.new('length', '%.1f' % ((pkg_dim[2]*10).round / 10.0)) if pkg_dim.size >= 3
+ dim << XmlNode.new('width', '%.1f' % ((pkg_dim[1]*10).round / 10.0)) if pkg_dim.size >= 2
+ dim << XmlNode.new('height', '%.1f' % ((pkg_dim[0]*10).round / 10.0)) if pkg_dim.size >= 1
+ end
+ end
+ end
+ el << XmlNode.new('mailing-tube', line_items.any?(&:tube?))
+ el << XmlNode.new('oversized', true) if line_items.any?(&:oversized?)
+ el << XmlNode.new('unpackaged', line_items.any?(&:unpackaged?))
+ end
+ end
+
+ def origin_node(location_hash)
+ origin = Location.new(sanitize_zip(location_hash))
+ XmlNode.new("origin-postal-code", origin.zip)
+ end
+
+ def destination_node(location_hash)
+ destination = Location.new(sanitize_zip(location_hash))
+ case destination.country_code
+ when 'CA'
+ XmlNode.new('destination') do |node|
+ node << XmlNode.new('domestic') do |x|
+ x << XmlNode.new('postal-code', destination.postal_code)
+ end
+ end
+
+ when 'US'
+ XmlNode.new('destination') do |node|
+ node << XmlNode.new('united-states') do |x|
+ x << XmlNode.new('zip-code', destination.postal_code)
+ end
+ end
+
+ else
+ XmlNode.new('destination') do |dest|
+ dest << XmlNode.new('international') do |dom|
+ dom << XmlNode.new('country-code', destination.country_code)
+ end
+ end
+ end
+ end
+
+ def services_node(services)
+ XmlNode.new('services') do |node|
+ services.each {|code| node << XmlNode.new('service-code', code)}
+ end
+ end
+
+ def shipping_options_node(available_options, options = {})
+ return if (options.symbolize_keys.keys & available_options).empty?
+ XmlNode.new('options') do |el|
+
+ if options[:cod] && options[:cod_amount]
+ el << XmlNode.new('option') do |opt|
+ opt << XmlNode.new('option-code', 'COD')
+ opt << XmlNode.new('option-amount', options[:cod_amount])
+ opt << XmlNode.new('option-qualifier-1', options[:cod_includes_shipping]) unless options[:cod_includes_shipping].blank?
+ opt << XmlNode.new('option-qualifier-2', options[:cod_method_of_payment]) unless options[:cod_method_of_payment].blank?
+ end
+ end
+
+ if options[:cov]
+ el << XmlNode.new('option') do |opt|
+ opt << XmlNode.new('option-code', 'COV')
+ opt << XmlNode.new('option-amount', options[:cov_amount]) unless options[:cov_amount].blank?
+ end
+ end
+
+ if options[:d2po]
+ el << XmlNode.new('option') do |opt|
+ opt << XmlNode.new('option-code', 'D2PO')
+ opt << XmlNode.new('option-qualifier-2'. options[:d2po_office_id]) unless options[:d2po_office_id].blank?
+ end
+ end
+
+ [:so, :dc, :pa18, :pa19, :hfp, :dns, :lad, :rase, :rts, :aban].each do |code|
+ if options[code]
+ el << XmlNode.new('option') do |opt|
+ opt << XmlNode.new('option-code', code.to_s.upcase)
+ end
+ end
+ end
+ end
+ end
+
+ def expected_date_from_node(node)
+ if service = node.elements['service-standard']
+ expected_date = service.get_text("expected-delivery-date").to_s
+ else
+ expected_date = nil
+ end
+ end
+
+ def sanitize_zip(hash)
+ [:postal_code, :zip].each do |attr|
+ hash[attr].gsub!(/\s+/,'') if hash[attr]
+ end
+ hash
+ end
+
+ def sanitize_weight_kg(kg)
+ return kg == 0 ? 0.001 : kg;
+ end
+
+ def sanitize_price_from_cents(value)
+ return value == 0 ? 0.01 : value.round / 100.0
+ end
+
+ end
+
+ module CPPWSErrorResponse
+ attr_accessor :error_code
+ def handle_error(message, options)
+ @error_code = options[:code]
+ end
+ end
+
+ class CPPWSRateResponse < RateResponse
+ include CPPWSErrorResponse
+
+ def initialize(success, message, params = {}, options = {})
+ handle_error(message, options)
+ super
+ end
+ end
+
+ class CPPWSTrackingResponse < TrackingResponse
+ include CPPWSErrorResponse
+
+ attr_reader :service_name, :expected_date, :changed_date, :change_reason, :customer_number
+
+ def initialize(success, message, params = {}, options = {})
+ handle_error(message, options)
+ super
+ @service_name = options[:service_name]
+ @expected_date = options[:expected_date]
+ @changed_date = options[:changed_date]
+ @change_reason = options[:change_reason]
+ @customer_number = options[:customer_number]
+ end
+ end
+
+ class CPPWSShippingResponse < ShippingResponse
+ include CPPWSErrorResponse
+ attr_reader :label_url, :details_url, :receipt_url
+ def initialize(success, message, params = {}, options = {})
+ handle_error(message, options)
+ super
+ @label_url = options[:label_url]
+ @details_url = options[:details_url]
+ @receipt_url = options[:receipt_url]
+ end
+ end
+
+ class CPPWSRegisterResponse < Response
+ include CPPWSErrorResponse
+ attr_reader :token_id
+ def initialize(success, message, params = {}, options = {})
+ handle_error(message, options)
+ super
+ @token_id = options[:token_id]
+ end
+
+ def redirect_url(customer_id, return_url)
+ "http://www.canadapost.ca/cpotools/apps/drc/merchant?return-url=#{CGI::escape(return_url)}&token-id=#{token_id}&platform-id=#{customer_id}"
+ end
+ end
+
+ class CPPWSMerchantDetailsResponse < Response
+ include CPPWSErrorResponse
+ attr_reader :customer_number, :contract_number, :username, :password, :has_default_credit_card
+ def initialize(success, message, params = {}, options = {})
+ handle_error(message, options)
+ super
+ @customer_number = options[:customer_number]
+ @contract_number = options[:contract_number]
+ @username = options[:username]
+ @password = options[:password]
+ @has_default_credit_card = options[:has_default_credit_card]
+ end
+ end
+
+ class InvalidPinFormatError < StandardError; end
+ class MissingCustomerNumberError < StandardError; end
+ class MissingShippingNumberError < StandardError; end
+ class MissingTokenIdError < StandardError; end
+
+ end
+end
View
121 lib/active_shipping/shipping/package.rb
@@ -1,5 +1,84 @@
module ActiveMerchant #:nodoc:
module Shipping #:nodoc:
+ # A package item is a unique item(s) that is physically in a package.
+ # A single package can have many items. This is only required
+ # for shipping methods (label creation) right now.
+ class PackageItem
+ include Quantified
+
+ attr_reader :sku, :hs_code, :value, :name, :weight, :quantity, :options
+
+ def initialize(name, grams_or_ounces, value, quantity, options = {})
+ @name = name
+
+ imperial = (options[:units] == :imperial) ||
+ (grams_or_ounces.respond_to?(:unit) && m.unit.to_sym == :imperial)
+
+ @unit_system = imperial ? :imperial : :metric
+
+ @weight = attribute_from_metric_or_imperial(grams_or_ounces, Mass, :grams, :ounces)
+
+ @value = Package.cents_from(value)
+ @quantity = quantity > 0 ? quantity : 1
+
+ @sku = options[:sku]
+ @hs_code = options[:hs_code]
+ @options = options
+ end
+
+ def value_per_unit
+ @value > 0 ? @value / @quantity : @value
+ end
+
+ def weight(options = {})
+ case options[:type]
+ when nil, :actual
+ @weight
+ when :volumetric, :dimensional
+ @volumetric_weight ||= begin
+ m = Mass.new((centimetres(:box_volume) / 6.0), :grams)
+ @unit_system == :imperial ? m.in_ounces : m
+ end
+ when :billable
+ [ weight, weight(:type => :volumetric) ].max
+ end
+ end
+ alias_method :mass, :weight
+
+ def ounces(options={})
+ weight(options).in_ounces.amount
+ end
+ alias_method :oz, :ounces
+
+ def grams(options={})
+ weight(options).in_grams.amount
+ end
+ alias_method :g, :grams
+
+ def pounds(options={})
+ weight(options).in_pounds.amount
+ end
+ alias_method :lb, :pounds
+ alias_method :lbs, :pounds
+
+ def kilograms(options={})
+ weight(options).in_kilograms.amount
+ end
+ alias_method :kg, :kilograms
+ alias_method :kgs, :kilograms
+
+ private
+
+ def attribute_from_metric_or_imperial(obj, klass, metric_unit, imperial_unit)
+ if obj.is_a?(klass)
+ return value
+ else
+ return klass.new(obj, (@unit_system == :imperial ? imperial_unit : metric_unit))
+ end
+ end
+
+ end
+
class Package
include Quantified
@@ -15,16 +94,30 @@ def initialize(grams_or_ounces, dimensions, options = {})
@options = options
@dimensions = [dimensions].flatten.reject {|d| d.nil?}
+
imperial = (options[:units] == :imperial) ||
- ([grams_or_ounces, *dimensions].all? {|m| m.respond_to?(:unit) && m.unit.to_sym == :imperial})
+ ([grams_or_ounces, *dimensions].all? {|m| m.respond_to?(:unit) && m.unit.to_sym == :imperial})
- @unit_system = imperial ? :imperial : :metric
+ weight_imperial = dimensions_imperial = imperial if options.include?(:units)
+
+ if options.include?(:weight_units)
+ weight_imperial = (options[:weight_units] == :imperial) ||
+ (grams_or_ounces.respond_to?(:unit) && m.unit.to_sym == :imperial)
+ end
+
+ if options.include?(:dim_units)
+ dimensions_imperial = (options[:dim_units] == :imperial) ||
+ (dimensions && dimensions.all? {|m| m.respond_to?(:unit) && m.unit.to_sym == :imperial})
+ end
- @weight = attribute_from_metric_or_imperial(grams_or_ounces, Mass, :grams, :ounces)
+ @weight_unit_system = weight_imperial ? :imperial : :metric
+ @dimensions_unit_system = dimensions_imperial ? :imperial : :metric
+
+ @weight = attribute_from_metric_or_imperial(grams_or_ounces, Mass, @weight_unit_system, :grams, :ounces)
if @dimensions.blank?
- @dimensions = [Length.new(0, (imperial ? :inches : :centimetres))] * 3
+ @dimensions = [Length.new(0, (dimensions_imperial ? :inches : :centimetres))] * 3
else
process_dimensions
end
@@ -33,8 +126,18 @@ def initialize(grams_or_ounces, dimensions, options = {})
@currency = options[:currency] || (options[:value].currency if options[:value].respond_to?(:currency))
@cylinder = (options[:cylinder] || options[:tube]) ? true : false
@gift = options[:gift] ? true : false
+ @oversized = options[:oversized] ? true : false
+ @unpackaged = options[:unpackaged] ? true : false
end
+ def unpackaged?
+ @unpackaged
+ end
+
+ def oversized?
+ @oversized
+ end
+
def cylinder?
@cylinder
end
@@ -83,7 +186,7 @@ def weight(options = {})
when :volumetric, :dimensional
@volumetric_weight ||= begin
m = Mass.new((centimetres(:box_volume) / 6.0), :grams)
- @unit_system == :imperial ? m.in_ounces : m
+ @weight_unit_system == :imperial ? m.in_ounces : m
end
when :billable
[ weight, weight(:type => :volumetric) ].max
@@ -108,12 +211,12 @@ def self.cents_from(money)
end
private
-
- def attribute_from_metric_or_imperial(obj, klass, metric_unit, imperial_unit)
+
+ def attribute_from_metric_or_imperial(obj, klass, unit_system, metric_unit, imperial_unit)
if obj.is_a?(klass)
return obj
else
- return klass.new(obj, (@unit_system == :imperial ? imperial_unit : metric_unit))
+ return klass.new(obj, (unit_system == :imperial ? imperial_unit : metric_unit))
end
end
@@ -132,7 +235,7 @@ def measure(measurement, ary)
def process_dimensions
@dimensions = @dimensions.map do |l|
- attribute_from_metric_or_imperial(l, Length, :centimetres, :inches)
+ attribute_from_metric_or_imperial(l, Length, @dimensions_unit_system, :centimetres, :inches)
end.sort
# [1,2] => [1,1,2]
# [5] => [5,5,5]
View
16 lib/active_shipping/shipping/shipping_response.rb
@@ -0,0 +1,16 @@
+module ActiveMerchant #:nodoc:
+ module Shipping
+
+ class ShippingResponse < Response
+ attr_reader :shipping_id # string
+ attr_reader :tracking_number # string
+
+ def initialize(success, message, params = {}, options = {})
+ @shipping_id = options[:shipping_id]
+ @tracking_number = options[:tracking_number]
+ super
+ end
+ end
+
+ end
+end
View
6 test/fixtures.yml
@@ -16,4 +16,8 @@ shipwire:
canada_post:
login: CPC_DEMO_XML
new_zealand_post:
- key: '4d9dc0f0-dda0-012e-066f-000c29b44ac0'
+canada_post_pws:
+ platform_id: 12345678
+ api_key: 1234789
+ secret: 1245743
View
BIN  test/fixtures/files/label1.pdf
Binary file not shown
View
112 test/fixtures/xml/canadapost_pws/dnc_tracking_details_en.xml
@@ -0,0 +1,112 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<tracking-detail xmlns="http://www.canadapost.ca/ws/track">
+ <pin>1680678172650919</pin>
+ <active-exists>1</active-exists>
+ <archive-exists/>
+ <changed-expected-date/>
+ <destination-postal-id>K0B1R0</destination-postal-id>
+ <expected-delivery-date>2011-01-12</expected-delivery-date>
+ <changed-expected-delivery-reason/>
+ <mailed-by-customer-number>0001680678</mailed-by-customer-number>
+ <mailed-on-behalf-of-customer-number>0001680678</mailed-on-behalf-of-customer-number>
+ <original-pin/>
+ <service-name>Xpresspost</service-name>
+ <service-name-2>Xpresspost</service-name-2>
+ <customer-ref-1/>
+ <customer-ref-2/>
+ <return-pin/>
+ <signature-image-exists>false</signature-image-exists>
+ <suppress-signature>true</suppress-signature>
+ <delivery-options>
+ <item>
+ <delivery-option/>
+ <delivery-option-description/>
+ </item>
+ </delivery-options>
+ <significant-events>
+ <occurrence>
+ <event-identifier>1498</event-identifier>
+ <event-date>2011-01-27</event-date>
+ <event-time>14:02:29</event-time>
+ <event-time-zone>EST</event-time-zone>
+ <event-description>Item successfully delivered</event-description>
+ <signatory-name/>
+ <event-site>VANKLEEK HILL</event-site>
+ <event-province>ON</event-province>
+ <event-retail-location-id/>
+ <event-retail-name/>
+ </occurrence>
+ <occurrence>
+ <event-identifier>0156</event-identifier>
+ <event-date>2011-01-18</event-date>
+ <event-time>07:57:50</event-time>
+ <event-time-zone>EST</event-time-zone>
+ <event-description>Final Notice; Item will be returned to sender if not collected within 10 days</event-description>
+ <signatory-name/>
+ <event-site>VANKLEEK HILL</event-site>
+ <event-province>ON</event-province>
+ <event-retail-location-id>0000315052</event-retail-location-id>
+ <event-retail-name>VANKLEEK HILL PO</event-retail-name>
+ </occurrence>
+ <occurrence>
+ <event-identifier>1479</event-identifier>
+ <event-date>2011-01-11</event-date>
+ <event-time>11:59:59</event-time>
+ <event-time-zone>EST</event-time-zone>
+ <event-description>Attempted delivery. Notice card left indicating where item can be picked up.</event-description>
+ <signatory-name/>
+ <event-site>VANKLEEK HILL</event-site>
+ <event-province>ON</event-province>
+ <event-retail-location-id/>
+ <event-retail-name/>
+ </occurrence>
+ <occurrence>
+ <event-identifier>1701</event-identifier>
+ <event-date>2011-01-11</event-date>
+ <event-time>11:55:51</event-time>
+ <event-time-zone>EST</event-time-zone>
+ <event-description>Item available for pickup at Post Office</event-description>
+ <signatory-name/>
+ <event-site>VANKLEEK HILL</event-site>
+ <event-province>ON</event-province>
+ <event-retail-location-id/>
+ <event-retail-name/>
+ </occurrence>
+ <occurrence>
+ <event-identifier>0100</event-identifier>
+ <event-date>2011-01-10</event-date>
+ <event-time>18:54:00</event-time>
+ <event-time-zone>EST</event-time-zone>
+ <event-description>Item processed at postal facility</event-description>
+ <signatory-name/>
+ <event-site>MISSISSAUGA</event-site>
+ <event-province>ON</event-province>
+ <event-retail-location-id/>
+ <event-retail-name/>
+ </occurrence>
+ <occurrence>
+ <event-identifier>2300</event-identifier>
+ <event-date>2011-01-10</event-date>
+ <event-time>17:41:50</event-time>
+ <event-time-zone>EST</event-time-zone>
+ <event-description>Item picked up by Canada Post</event-description>
+ <signatory-name/>
+ <event-site>MISSISSAUGA</event-site>
+ <event-province>ON</event-province>
+ <event-retail-location-id/>
+ <event-retail-name/>
+ </occurrence>
+ <occurrence>
+ <event-identifier>3000</event-identifier>
+ <event-date>2011-01-10</event-date>
+ <event-time>15:00:00</event-time>
+ <event-time-zone>EST</event-time-zone>
+ <event-description>Order information received by Canada Post</event-description>
+ <signatory-name/>
+ <event-site>MISSISSAUGA</event-site>
+ <event-province>ON</event-province>
+ <event-retail-location-id/>
+ <event-retail-name/>
+ </occurrence>
+ </significant-events>
+</tracking-detail>
View
7 test/fixtures/xml/canadapost_pws/merchant_details_error.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<messages xmlns="http://www.canadapost.ca/ws/messages">
+ <message>
+ <code>007</code>
+ <description>Merchant Details Error</description>
+ </message>
+</messages>
View
7 test/fixtures/xml/canadapost_pws/merchant_details_response.xml
@@ -0,0 +1,7 @@
+<merchant-info>
+<customer-number>1234567890</customer-number>
+<contract-number>1234567890-</contract-number>
+<merchant-username>1234567890123456</merchant-username>
+<merchant-password>12343567890123456789012</merchant-password>
+<has-default-credit-card>false</has-default-credit-card>
+</merchant-info>
View
13 test/fixtures/xml/canadapost_pws/option_response.xml
@@ -0,0 +1,13 @@
+<option>
+<option-code>SO</option-code>
+<option-name>Signature option</option-name>
+<option-class>FEAT</option-class>
+<prints-on-label>true</prints-on-label>
+<qualifier-required>false</qualifier-required>
+<conflicting-options>
+<option-code>LAD</option-code>
+</conflicting-options>
+<prerequisite-options>
+<option-code>DC</option-code>
+</prerequisite-options>
+</option>
View
7 test/fixtures/xml/canadapost_pws/option_response_no_conflicts.xml
@@ -0,0 +1,7 @@
+<option>
+<option-code>SO</option-code>
+<option-name>Signature option</option-name>
+<option-class>FEAT</option-class>
+<prints-on-label>true</prints-on-label>
+<qualifier-required>false</qualifier-required>
+</option>
View
190 test/fixtures/xml/canadapost_pws/rates_info.xml
@@ -0,0 +1,190 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<price-quotes xmlns="http://www.canadapost.ca/ws/ship/rate">
+ <price-quote>
+ <service-code>DOM.EP</service-code>
+ <service-link rel="service" href="https://soa-gw.canadapost.ca/rs/ship/service/DOM.EP?country=CA" media-type="application/vnd.cpc.ship.rate+xml"/>
+ <service-name>Expedited Parcel</service-name>
+ <price-details>
+ <base>10.97</base>
+ <taxes>
+ <gst>0.00</gst>
+ <pst>0.00</pst>
+ <hst percent="12.0000">1.39</hst>
+ </taxes>
+ <due>13.01</due>
+ <options>
+ <option>
+ <option-code>DC</option-code>
+ <option-name>Delivery confirmation</option-name>
+ <option-price>0</option-price>
+ </option>
+ </options>
+ <adjustments>
+ <adjustment>
+ <adjustment-code>AUTDISC</adjustment-code>
+ <adjustment-name>Automation discount</adjustment-name>
+ <adjustment-cost>-0.33</adjustment-cost>
+ <qualifier>
+ <percent>3.000</percent>
+ </qualifier>
+ </adjustment>
+ <adjustment>
+ <adjustment-code>FUELSC</adjustment-code>
+ <adjustment-name>Fuel surcharge</adjustment-name>
+ <adjustment-cost>0.98</adjustment-cost>
+ <qualifier>
+ <percent>9.25</percent>
+ </qualifier>
+ </adjustment>
+ </adjustments>
+ </price-details>
+ <weight-details/>
+ <service-standard>
+ <am-delivery>false</am-delivery>
+ <guaranteed-delivery>true</guaranteed-delivery>
+ <expected-transit-time>5</expected-transit-time>
+ <expected-delivery-date>2012-01-18</expected-delivery-date>
+ </service-standard>
+ </price-quote>
+ <price-quote>
+ <service-code>DOM.PC</service-code>
+ <service-link rel="service" href="https://soa-gw.canadapost.ca/rs/ship/service/DOM.PC?country=CA" media-type="application/vnd.cpc.ship.rate+xml"/>
+ <service-name>Priority Courier</service-name>
+ <price-details>
+ <base>30.61</base>
+ <taxes>
+ <gst>0.00</gst>
+ <pst>0.00</pst>
+ <hst percent="12.0000">4.07</hst>
+ </taxes>
+ <due>37.99</due>
+ <options>
+ <option>
+ <option-code>DC</option-code>
+ <option-name>Delivery confirmation</option-name>
+ <option-price>0</option-price>
+ </option>
+ </options>
+ <adjustments>
+ <adjustment>
+ <adjustment-code>AUTDISC</adjustment-code>
+ <adjustment-name>Automation discount</adjustment-name>
+ <adjustment-cost>-0.92</adjustment-cost>
+ <qualifier>
+ <percent>3.000</percent>
+ </qualifier>
+ </adjustment>
+ <adjustment>
+ <adjustment-code>FUELSC</adjustment-code>
+ <adjustment-name>Fuel surcharge</adjustment-name>
+ <adjustment-cost>4.23</adjustment-cost>
+ <qualifier>
+ <percent>14.25</percent>
+ </qualifier>
+ </adjustment>
+ </adjustments>
+ </price-details>
+ <weight-details/>
+ <service-standard>
+ <am-delivery>true</am-delivery>
+ <guaranteed-delivery>true</guaranteed-delivery>
+ <expected-transit-time>1</expected-transit-time>
+ <expected-delivery-date>2012-01-12</expected-delivery-date>
+ </service-standard>
+ </price-quote>
+ <price-quote>
+ <service-code>DOM.RP</service-code>
+ <service-link rel="service" href="https://soa-gw.canadapost.ca/rs/ship/service/DOM.RP?country=CA" media-type="application/vnd.cpc.ship.rate+xml"/>
+ <service-name>Regular Parcel</service-name>
+ <price-details>
+ <base>10.97</base>
+ <taxes>
+ <gst>0.00</gst>
+ <pst>0.00</pst>
+ <hst percent="12.0000">1.39</hst>
+ </taxes>
+ <due>13.01</due>
+ <options>
+ <option>
+ <option-code>DC</option-code>
+ <option-name>Delivery confirmation</option-name>
+ <option-price>0</option-price>
+ <qualifier>
+ <included>true</included>
+ </qualifier>
+ </option>
+ </options>
+ <adjustments>
+ <adjustment>
+ <adjustment-code>AUTDISC</adjustment-code>
+ <adjustment-name>Automation discount</adjustment-name>
+ <adjustment-cost>-0.33</adjustment-cost>
+ <qualifier>
+ <percent>3.000</percent>
+ </qualifier>
+ </adjustment>
+ <adjustment>
+ <adjustment-code>FUELSC</adjustment-code>
+ <adjustment-name>Fuel surcharge</adjustment-name>
+ <adjustment-cost>0.98</adjustment-cost>
+ <qualifier>
+ <percent>9.25</percent>
+ </qualifier>
+ </adjustment>
+ </adjustments>
+ </price-details>
+ <weight-details/>
+ <service-standard>
+ <am-delivery>false</am-delivery>
+ <guaranteed-delivery>false</guaranteed-delivery>
+ <expected-transit-time>7</expected-transit-time>
+ <expected-delivery-date>2012-01-20</expected-delivery-date>
+ </service-standard>
+ </price-quote>
+ <price-quote>
+ <service-code>DOM.XP</service-code>
+ <service-link rel="service" href="https://soa-gw.canadapost.ca/rs/ship/service/DOM.XP?country=CA" media-type="application/vnd.cpc.ship.rate+xml"/>
+ <service-name>Xpresspost</service-name>
+ <price-details>
+ <base>18.07</base>
+ <taxes>
+ <gst>0.00</gst>
+ <pst>0.00</pst>
+ <hst percent="12.0000">2.40</hst>
+ </taxes>
+ <due>22.43</due>
+ <options>
+ <option>
+ <option-code>DC</option-code>
+ <option-name>Delivery confirmation</option-name>
+ <option-price>0</option-price>
+ </option>
+ </options>
+ <adjustments>
+ <adjustment>
+ <adjustment-code>AUTDISC</adjustment-code>
+ <adjustment-name>Automation discount</adjustment-name>
+ <adjustment-cost>-0.54</adjustment-cost>
+ <qualifier>
+ <percent>3.000</percent>
+ </qualifier>
+ </adjustment>
+ <adjustment>
+ <adjustment-code>FUELSC</adjustment-code>
+ <adjustment-name>Fuel surcharge</adjustment-name>
+ <adjustment-cost>2.50</adjustment-cost>
+ <qualifier>
+ <percent>14.25</percent>
+ </qualifier>
+ </adjustment>
+ </adjustments>
+ </price-details>
+ <weight-details/>
+ <service-standard>
+ <am-delivery>false</am-delivery>
+ <guaranteed-delivery>true</guaranteed-delivery>
+ <expected-transit-time>2</expected-transit-time>
+ <expected-delivery-date>2012-01-13</expected-delivery-date>
+ </service-standard>
+ </price-quote>
+</price-quotes>
View
7 test/fixtures/xml/canadapost_pws/rates_info_error.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<messages xmlns="http://www.canadapost.ca/ws/messages">
+ <message>
+ <code>AA004</code>
+ <description>You cannot mail on behalf of the requested customer.</description>
+ </message>
+</messages>
View
42 test/fixtures/xml/canadapost_pws/receipt_response.xml
@@ -0,0 +1,42 @@
+<non-contract-shipment-receipt>
+<final-shipping-point>J4W4T0</final-shipping-point>
+<shipping-point-name>BP BROSSARD</shipping-point-name>
+<shipping-point-id>0192</shipping-point-id>
+<mailed-by-customer>0001111111</mailed-by-customer>
+<service-code>DOM.EP</service-code>
+<rated-weight>15.000</rated-weight>
+<base-amount>18.10</base-amount>
+<pre-tax-amount>19.46</pre-tax-amount>
+<gst-amount>0.00</gst-amount>
+<pst-amount>0.00</pst-amount>
+<hst-amount>2.53</hst-amount>
+<priced-options>
+<priced-option>
+<option-code>DC</option-code>
+<option-price>0</option-price>
+</priced-option>
+</priced-options>
+<adjustments>
+<adjustment>
+<adjustment-code>FUELSC</adjustment-code>
+<adjustment-amount>1.36</adjustment-amount>
+</adjustment>
+</adjustments>
+<cc-receipt-details>
+<merchant-name>Canada Post Corporation</merchant-name>
+<merchant-url>www.canadapost.ca</merchant-url>
+<name-on-card>John Doe</name-on-card>
+<auth-code>076838</auth-code>
+<auth-timestamp>2012-03-13T08:27:20-05:00</auth-timestamp>
+<card-type>VIS</card-type>
+<charge-amount>21.99</charge-amount>
+<currency>CAD</currency>
+<transaction-type>Sale</transaction-type>
+</cc-receipt-details>
+<service-standard>
+<am-delivery>false</am-delivery>
+<guaranteed-delivery>true</guaranteed-delivery>
+<expected-transit-time>1</expected-transit-time>
+<expected-delivery-date>2012-03-14</expected-delivery-date>
+</service-standard>
+</non-contract-shipment-receipt>
View
36 test/fixtures/xml/canadapost_pws/receipt_response_no_priced_options.xml
@@ -0,0 +1,36 @@
+<non-contract-shipment-receipt>
+<final-shipping-point>J4W4T0</final-shipping-point>
+<shipping-point-name>BP BROSSARD</shipping-point-name>
+<shipping-point-id>0192</shipping-point-id>
+<mailed-by-customer>0001111111</mailed-by-customer>
+<service-code>DOM.EP</service-code>
+<rated-weight>15.000</rated-weight>
+<base-amount>18.10</base-amount>
+<pre-tax-amount>19.46</pre-tax-amount>
+<gst-amount>0.00</gst-amount>
+<pst-amount>0.00</pst-amount>
+<hst-amount>2.53</hst-amount>
+<adjustments>
+<adjustment>
+<adjustment-code>FUELSC</adjustment-code>
+<adjustment-amount>1.36</adjustment-amount>
+</adjustment>
+</adjustments>
+<cc-receipt-details>
+<merchant-name>Canada Post Corporation</merchant-name>
+<merchant-url>www.canadapost.ca</merchant-url>
+<name-on-card>John Doe</name-on-card>
+<auth-code>076838</auth-code>
+<auth-timestamp>2012-03-13T08:27:20-05:00</auth-timestamp>
+<card-type>VIS</card-type>
+<charge-amount>21.99</charge-amount>
+<currency>CAD</currency>
+<transaction-type>Sale</transaction-type>
+</cc-receipt-details>
+<service-standard>
+<am-delivery>false</am-delivery>
+<guaranteed-delivery>true</guaranteed-delivery>
+<expected-transit-time>1</expected-transit-time>
+<expected-delivery-date>2012-03-14</expected-delivery-date>
+</service-standard>
+</non-contract-shipment-receipt>
View
7 test/fixtures/xml/canadapost_pws/register_token_error.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<messages xmlns="http://www.canadapost.ca/ws/messages">
+ <message>
+ <code>007</code>
+ <description>Platform not active</description>
+ </message>
+</messages>
View
3  test/fixtures/xml/canadapost_pws/register_token_response.xml
@@ -0,0 +1,3 @@
+<token>
+<token-id>34536456345353534535</token-id>
+</token>
View
42 test/fixtures/xml/canadapost_pws/service_options_response.xml
@@ -0,0 +1,42 @@
+<service>
+<service-code>INT.XP</service-code>
+<service-name>Xpresspost International</service-name>
+<options>
+<option>
+<option-code>COV</option-code>
+<option-name>Coverage</option-name>
+<link rel="option" href="https://XX/rs/ship/option/COV" media-type="application/vnd.cpc.ship.rate+xml"></link>
+<mandatory>false</mandatory>
+<qualifier-required>true</qualifier-required>
+<qualifier-max>1000</qualifier-max>
+</option>
+<option>
+<option-code>DC</option-code>
+<option-name>Delivery confirmation</option-name>
+<link rel="option" href="https://XX/rs/ship/option/DC" media-type="application/vnd.cpc.ship.rate+xml"></link>
+<mandatory>true</mandatory>
+<qualifier-required>false</qualifier-required>
+</option>
+<option>
+<option-code>RASE</option-code>
+<option-name>Return at sender's expense</option-name>
+<link rel="option" href="https://XX/rs/ship/option/RASE" media-type="application/vnd.cpc.ship.rate+xml"></link>
+<mandatory>true</mandatory>
+<qualifier-required>false</qualifier-required>
+</option>
+</options>
+<restrictions>
+<weight-restriction min="0" max="30000"></weight-restriction>
+<dimensional-restrictions>
+<length min="0.1" max="150"></length>
+<width min="0.1" max="150"></width>
+<height min="0.1" max="150"></height>
+<length-plus-girth-max>300</length-plus-girth-max>
+<oversize-limit>100</oversize-limit>
+</dimensional-restrictions>
+<density-factor>6000</density-factor>
+<can-ship-in-mailing-tube>true</can-ship-in-mailing-tube>
+<can-ship-unpackaged>false</can-ship-unpackaged>
+<allowed-as-return-service>false</allowed-as-return-service>
+</restrictions>
+</service>
View
6 test/fixtures/xml/canadapost_pws/services_error.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<messages xmlns="http://www.canadapost.ca/ws/messages">
+<message>
+<code>8534</code>
+<description>A valid destination country must be supplied.</description></message></messages>
View
32 test/fixtures/xml/canadapost_pws/services_response.xml
@@ -0,0 +1,32 @@
+<services>
+<service>
+<service-code>INT.PW.ENV</service-code>
+<service-name>Priority Worldwide envelope INT'L</service-name>
+<link rel="service" href="https://XX/rs/ship/service/INT.PW.ENV?country=JP" media-type="application/vnd.cpc.ship.rate+xml"></link>
+</service>
+<service>
+<service-code>INT.PW.PAK</service-code>
+<service-name>Priority Worldwide pak INT'L</service-name>
+<link rel="service" href="https://XX/rs/ship/service/INT.PW.PAK?country=JP" media-type="application/vnd.cpc.ship.rate+xml"></link>
+</service>
+<service>
+<service-code>INT.PW.PARCEL</service-code>
+<service-name>Priority Worldwide parcel INT'L</service-name>
+<link rel="service" href="https://XX/rs/ship/service/INT.PW.PARCEL?country=JP" media-type="application/vnd.cpc.ship.rate+xml"></link>
+</service>
+<service>
+<service-code>INT.XP</service-code>
+<service-name>Xpresspost International</service-name>
+<link rel="service" href="https://XX/rs/ship/service/INT.XP?country=JP" media-type="application/vnd.cpc.ship.rate+xml"></link>
+</service>
+<service>
+<service-code>INT.SP.SURF</service-code>
+<service-name>Small Packet International Surface</service-name>
+<link rel="service" href="https://XX/rs/ship/service/INT.SP.SURF?country=JP" media-type="application/vnd.cpc.ship.rate+xml"></link>
+</service>
+<service>
+<service-code>INT.SP.AIR</service-code>
+<service-name>Small Packet International Air</service-name>
+<link rel="service" href="https://XX/rs/ship/service/INT.SP.AIR?country=JP" media-type="application/vnd.cpc.ship.rate+xml"></link>
+</service>
+</services>
View
69 test/fixtures/xml/canadapost_pws/shipment_domestic.xml
@@ -0,0 +1,69 @@
+<?xml version="1.0"?>
+<non-contract-shipment xmlns="http://www.canadapost.ca/ws/ncshipment">
+ <delivery-spec>
+ <service-code>INT.SP.AIR</service-code>
+ <sender>
+ <name>John Smith</name>
+ <company>test</company>
+ <contact-phone>613-555-1212</contact-phone>
+ <address-details>
+ <address-line-1>123 Elm St.</address-line-1>
+ <city>Ottawa</city>
+ <prov-state>ON</prov-state>
+ <postal-zip-code>K1P1J1</postal-zip-code>
+ </address-details>
+ </sender>
+ <destination>
+ <client-voice-number>011-123-123-1234</client-voice-number>
+ <address-details>
+ <address-line-1>321 Lake St.</address-line-1>
+ <city>Toronto</city>
+ <prov-state/>
+ <country-code>CA</country-code>
+ <postal-zip-code/>
+ </address-details>
+ </destination>
+ <options>
+ <option>
+ <option-code>DC</option-code>
+ </option>
+ <option>
+ <option-code>SO</option-code>
+ </option>
+ <option>
+ <option-code>RASE</option-code>
+ </option>
+ </options>
+ <parcel-characteristics>
+ <weight>1.000</weight>
+ <dimensions>
+ <length>25</length>
+ <width>25</width>
+ <height>25</height>
+ </dimensions>
+ <document>false</document>
+ <mailing-tube>false</mailing-tube>
+ <unpackaged>false</unpackaged>
+ </parcel-characteristics>
+ <preferences>
+ <show-packing-instructions>true</show-packing-instructions>
+ <show-postage-rate>false</show-postage-rate>
+ <show-insured-value>true</show-insured-value>
+ </preferences>
+ <customs>
+ <currency>CAD</currency>
+ <conversion-from-cad>1</conversion-from-cad>
+ <reason-for-export>GIF</reason-for-export>
+ <sku-list>
+ <item>
+ <hs-tariff-code>1234.12.12.12</hs-tariff-code>
+ <sku>some-value</sku>
+ <customs-description>Wood Post</customs-description>
+ <unit-weight>1.0</unit-weight>
+ <customs-value-per-unit>100</customs-value-per-unit>
+ <customs-number-of-units>1</customs-number-of-units>
+ </item>
+ </sku-list>
+ </customs>
+ </delivery-spec>
+</non-contract-shipment>
View
20 test/fixtures/xml/canadapost_pws/shipment_response.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<non-contract-shipment-info xmlns="http://www.canadapost.ca/ws/ncshipment">
+
+ <shipment-id>406951321983787352</shipment-id>
+ <tracking-pin>11111118901234</tracking-pin>
+ <links>
+ <link rel="self"
+ href="https://ct.soa-gw.canadapost.ca/rs/1234567890/ncshipment/406951321983787352"
+ media-type="application/vnd.cpc.ncshipment+xml" />
+ <link rel="details"
+ href="https://ct.soa-gw.canadapost.ca/rs/1234567890/ncshipment/406951321983787352/details"
+ media-type="application/vnd.cpc.ncshipment+xml" />
+ <link rel="receipt"
+ href="https://ct.soa-gw.canadapost.ca/rs/1234567890/ncshipment/406951321983787352/receipt"
+ media-type="application/vnd.cpc.ncshipment+xml" />
+ <link rel="label"
+ href="https://ct.soa-gw.canadapost.ca/ers/artifact/c70da5ed5a0d2c32/20238/0"
+ media-type="application/pdf" index="0" />
+ </links>
+</non-contract-shipment-info>
View
69 test/fixtures/xml/canadapost_pws/shipment_us.xml
@@ -0,0 +1,69 @@
+<?xml version="1.0"?>
+<non-contract-shipment xmlns="http://www.canadapost.ca/ws/ncshipment">
+ <delivery-spec>
+ <service-code>USA.XP</service-code>
+ <sender>
+ <name>John Smith</name>
+ <company>test</company>
+ <contact-phone>613-555-1212</contact-phone>
+ <address-details>
+ <address-line-1>123 Elm St.</address-line-1>
+ <city>Ottawa</city>
+ <prov-state>ON</prov-state>
+ <postal-zip-code>K1P1J1</postal-zip-code>
+ </address-details>
+ </sender>
+ <destination>
+ <client-voice-number>123-123-1234</client-voice-number>
+ <address-details>
+ <address-line-1>999 Wiltshire Blvd</address-line-1>
+ <city>Beverly Hills</city>
+ <prov-state>CA</prov-state>
+ <country-code>US</country-code>
+ <postal-zip-code>90210</postal-zip-code>
+ </address-details>
+ </destination>
+ <options>
+ <option>
+ <option-code>DC</option-code>
+ </option>
+ <option>
+ <option-code>SO</option-code>
+ </option>
+ <option>
+ <option-code>RASE</option-code>
+ </option>
+ </options>
+ <parcel-characteristics>
+ <weight>1.000</weight>
+ <dimensions>
+ <length>25</length>
+ <width>25</width>
+ <height>25</height>
+ </dimensions>
+ <document>false</document>
+ <mailing-tube>false</mailing-tube>
+ <unpackaged>false</unpackaged>
+ </parcel-characteristics>
+ <preferences>
+ <show-packing-instructions>true</show-packing-instructions>
+ <show-postage-rate>false</show-postage-rate>
+ <show-insured-value>true</show-insured-value>
+ </preferences>
+ <customs>
+ <currency>USD</currency>
+ <conversion-from-cad>1</conversion-from-cad>
+ <reason-for-export>GIF</reason-for-export>
+ <sku-list>
+ <item>
+ <hs-tariff-code>1234.12.12.12</hs-tariff-code>
+ <sku>some-value</sku>
+ <customs-description>Wood Post</customs-description>
+ <unit-weight>1.0</unit-weight>
+ <customs-value-per-unit>100</customs-value-per-unit>
+ <customs-number-of-units>1</customs-number-of-units>
+ </item>
+ </sku-list>
+ </customs>
+ </delivery-spec>
+</non-contract-shipment>
View
152 test/fixtures/xml/canadapost_pws/tracking_details_en.xml
@@ -0,0 +1,152 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<tracking-detail xmlns="http://www.canadapost.ca/ws/track">
+ <pin>1371134583769923</pin>
+ <active-exists>1</active-exists>
+ <archive-exists/>
+ <changed-expected-date>2011-02-11</changed-expected-date>
+ <destination-postal-id>G1K4M7</destination-postal-id>
+ <expected-delivery-date>2011-02-01</expected-delivery-date>
+ <changed-expected-delivery-reason>Customer addressing error found; attempting to correct</changed-expected-delivery-reason>
+ <mailed-by-customer-number>0001371134</mailed-by-customer-number>
+ <mailed-on-behalf-of-customer-number>0001371134</mailed-on-behalf-of-customer-number>
+ <original-pin/>
+ <service-name>Xpresspost</service-name>
+ <service-name-2>Xpresspost</service-name-2>
+ <customer-ref-1>955-0398</customer-ref-1>
+ <customer-ref-2/>
+ <return-pin/>
+ <signature-image-exists>true</signature-image-exists>
+ <suppress-signature>false</suppress-signature>
+ <delivery-options>
+ <item>
+ <delivery-option/>
+ <delivery-option-description/>
+ </item>
+ <item>
+ <delivery-option>CH_SGN_OPTION</delivery-option>
+ <delivery-option-description>Signature Required</delivery-option-description>
+ </item>
+ </delivery-options>
+ <significant-events>
+ <occurrence>
+ <event-identifier>1496</event-identifier>
+ <event-date>2011-02-03</event-date>
+ <event-time>11:59:59</event-time>
+ <event-time-zone>EST</event-time-zone>
+ <event-description>Item successfully delivered</event-description>
+ <signatory-name/>
+ <event-site>SAINTE-FOY</event-site>
+ <event-province>QC</event-province>
+ <event-retail-location-id/>
+ <event-retail-name/>
+ </occurrence>
+ <occurrence>
+ <event-identifier>20</event-identifier>
+ <event-date>2011-02-03</event-date>
+ <event-time>11:59:59</event-time>
+ <event-time-zone>EST</event-time-zone>
+ <event-description>Signature image recorded for Online viewing</event-description>
+ <signatory-name>HETU</signatory-name>
+ <event-site>SAINTE-FOY</event-site>
+ <event-province>QC</event-province>
+ <event-retail-location-id/>
+ <event-retail-name/>
+ </occurrence>
+ <occurrence>
+ <event-identifier>0174</event-identifier>
+ <event-date>2011-02-03</event-date>
+ <event-time>08:27:43</event-time>
+ <event-time-zone>EST</event-time-zone>
+ <event-description>Item out for delivery</event-description>
+ <signatory-name/>
+ <event-site>SAINTE-FOY</event-site>
+ <event-province>QC</event-province>
+ <event-retail-location-id/>
+ <event-retail-name/>
+ </occurrence>
+ <occurrence>
+ <event-identifier>0100</event-identifier>
+ <event-date>2011-02-02</event-date>
+ <event-time>14:45:48</event-time>
+ <event-time-zone>EST</event-time-zone>
+ <event-description>Item processed at postal facility</event-description>
+ <signatory-name/>
+ <event-site>QUEBEC</event-site>
+ <event-province>QC</event-province>
+ <event-retail-location-id/>
+ <event-retail-name/>
+ </occurrence>
+ <occurrence>
+ <event-identifier>0173</event-identifier>
+ <event-date>2011-02-02</event-date>
+ <event-time>06:19:57</event-time>
+ <event-time-zone>EST</event-time-zone>
+ <event-description>Customer addressing error found; attempting to correct. Possible delay</event-description>
+ <signatory-name/>
+ <event-site>QUEBEC</event-site>
+ <event-province>QC</event-province>
+ <event-retail-location-id/>
+ <event-retail-name/>
+ </occurrence>
+ <occurrence>
+ <event-identifier>1496</event-identifier>
+ <event-date>2011-02-01</event-date>
+ <event-time>07:59:52</event-time>
+ <event-time-zone>EST</event-time-zone>
+ <event-description>Item successfully delivered</event-description>
+ <signatory-name/>
+ <event-site>QUEBEC</event-site>
+ <event-province>QC</event-province>
+ <event-retail-location-id/>
+ <event-retail-name/>
+ </occurrence>
+ <occurrence>
+ <event-identifier>20</event-identifier>
+ <event-date>2011-02-01</event-date>
+ <event-time>07:59:52</event-time>
+ <event-time-zone>EST</event-time-zone>
+ <event-description>Signature image recorded for Online viewing</event-description>
+ <signatory-name>R GREGOIRE</signatory-name>
+ <event-site>QUEBEC</event-site>
+ <event-province>QC</event-province>
+ <event-retail-location-id/>
+ <event-retail-name/>
+ </occurrence>
+ <occurrence>
+ <event-identifier>0500</event-identifier>
+ <event-date>2011-02-01</event-date>
+ <event-time>07:51:23</event-time>
+ <event-time-zone>EST</event-time-zone>
+ <event-description>Out for delivery</event-description>
+ <signatory-name/>
+ <event-site>QUEBEC</event-site>
+ <event-province>QC</event-province>
+ <event-retail-location-id/>
+ <event-retail-name/>
+ </occurrence>
+ <occurrence>
+ <event-identifier>2300</event-identifier>
+ <event-date>2011-01-31</event-date>
+ <event-time>17:06:02</event-time>
+ <event-time-zone>EST</event-time-zone>
+ <event-description>Item picked up by Canada Post</event-description>
+ <signatory-name/>
+ <event-site>MONTREAL</event-site>
+ <event-province>QC</event-province>
+ <event-retail-location-id/>
+ <event-retail-name/>
+ </occurrence>
+ <occurrence>
+ <event-identifier>3000</event-identifier>
+ <event-date>2011-01-31</event-date>
+ <event-time>14:34:57</event-time>
+ <event-time-zone>EST</event-time-zone>
+ <event-description>Order information received by Canada Post</event-description>
+ <signatory-name/>
+ <event-site>LACHINE</event-site>
+ <event-province>QC</event-province>
+ <event-retail-location-id/>
+ <event-retail-name/>
+ </occurrence>
+ </significant-events>
+</tracking-detail>
View
7 test/fixtures/xml/canadapost_pws/tracking_details_en_error.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<messages xmlns="http://www.canadapost.ca/ws/messages">
+ <message>
+ <code>004</code>
+ <description>No Pin History</description>
+ </message>
+</messages>
View
156 test/fixtures/xml/canadapost_pws/tracking_details_fr.xml
@@ -0,0 +1,156 @@
+ <?xml version="1.0" encoding="UTF-8"?>
+<tracking-detail xmlns="http://www.canadapost.ca/ws/track">
+ <pin>1371134583769923</pin>
+ <active-exists />
+ <archive-exists>1</archive-exists>
+ <changed-expected-date>2011-02-11</changed-expected-date>
+ <destination-postal-id>G1K4M7</destination-postal-id>
+ <expected-delivery-date>2011-02-01</expected-delivery-date>
+ <changed-expected-delivery-reason>Erreur d'adressage du client.
+ Tentative de correction</changed-expected-delivery-reason>
+ <mailed-by-customer-number />
+ <mailed-on-behalf-of-customer-number>0001371134</mailed-on-behalf-of-customer-number>
+ <original-pin />
+ <service-name>Xpresspost</service-name>
+ <service-name-2>Xpresspost</service-name-2>
+ <customer-ref-1>955-0398</customer-ref-1>
+ <customer-ref-2 />
+ <return-pin />
+ <signature-image-exists>false</signature-image-exists>
+ <suppress-signature>false</suppress-signature>
+ <delivery-options>
+ <item>
+ <delivery-option>CH_SGN_OPTION</delivery-option>
+ <delivery-option-description>Signature Requise</delivery-option-description>
+ </item>
+ </delivery-options>
+ <significant-events>
+ <occurrence>
+ <event-identifier>1496</event-identifier>
+ <event-date>2011-02-03</event-date>
+ <event-time>11:59:59</event-time>
+ <event-time-zone>EST</event-time-zone>
+ <event-description>Article livr&#233; avec succ&#232;s</event-description>
+ <signatory-name>HETU</signatory-name>
+ <event-site>SAINTE-FOY</event-site>
+ <event-province>QC</event-province>
+ <event-retail-location-id />
+ <event-retail-name />
+ </occurrence>
+ <occurrence>
+ <event-identifier>20</event-identifier>
+ <event-date>2011-02-03</event-date>
+ <event-time>11:59:59</event-time>
+ <event-time-zone>EST</event-time-zone>
+ <event-description>Image de la signature enregistr&#233;e pour
+ consultation en ligne</event-description>
+ <signatory-name>HETU</signatory-name>
+ <event-site>SAINTE-FOY</event-site>
+ <event-province>QC</event-province>
+ <event-retail-location-id />
+ <event-retail-name />
+ </occurrence>
+ <occurrence>
+ <event-identifier>0174</event-identifier>
+ <event-date>2011-02-03</event-date>
+ <event-time>08:27:43</event-time>
+ <event-time-zone>EST</event-time-zone>
+ <event-description>Article sorti pour livraison</event-description>
+ <signatory-name />
+ <event-site>SAINTE-FOY</event-site>
+ <event-province>QC</event-province>
+ <event-retail-location-id />
+ <event-retail-name />
+ </occurrence>
+ <occurrence>
+ <event-identifier>0100</event-identifier>
+ <event-date>2011-02-02</event-date>
+ <event-time>14:45:48</event-time>
+ <event-time-zone>EST</event-time-zone>
+ <event-description>Article trait&#233; &#224; l'installation postale</event-description>
+ <signatory-name />
+ <event-site>QUEBEC</event-site>
+ <event-province>QC</event-province>
+ <event-retail-location-id />
+ <event-retail-name />
+ </occurrence>
+ <occurrence>
+ <event-identifier>0173</event-identifier>
+ <event-date>2011-02-02</event-date>
+ <event-time>06:19:57</event-time>
+ <event-time-zone>EST</event-time-zone>
+ <event-description>Erreur d'adressage du client; tentative de
+ correction. D&#233;lai possible</event-description>
+ <signatory-name />
+ <event-site>QUEBEC</event-site>
+ <event-province>QC</event-province>
+ <event-retail-location-id />
+ <event-retail-name />
+ </occurrence>
+ <occurrence>
+ <event-identifier>1496</event-identifier>
+ <event-date>2011-02-01</event-date>
+ <event-time>07:59:52</event-time>
+ <event-time-zone>EST</event-time-zone>
+ <event-description>Article livr&#233; avec succ&#232;s</event-description>
+ <signatory-name>R GREGOIRE</signatory-name>
+ <event-site>QUEBEC</event-site>
+ <event-province>QC</event-province>
+ <event-retail-location-id />
+ <event-retail-name />
+ </occurrence>
+ <occurrence>
+ <event-identifier>20</event-identifier>
+ <event-date>2011-02-01</event-date>
+ <event-time>07:59:52</event-time>
+ <event-time-zone>EST</event-time-zone>
+ <event-description>Image de la signature enregistr&#233;e pour
+ consultation en ligne</event-description>
+ <signatory-name>R GREGOIRE</signatory-name>
+ <event-site>QUEBEC</event-site>
+ <event-province>QC</event-province>
+ <event-retail-location-id />
+ <event-retail-name />
+ </occurrence>
+ <occurrence>
+ <event-identifier>0500</event-identifier>
+ <event-date>2011-02-01</event-date>
+ <event-time>07:51:23</event-time>
+ <event-time-zone>EST</event-time-zone>
+ <event-description>Sorti pour livraison</event-description>
+ <signatory-name />
+ <event-site>QUEBEC</event-site>
+ <event-province>QC</event-province>
+ <event-retail-location-id />
+ <event-retail-name />
+ </occurrence>
+ <occurrence>
+ <event-identifier>2300</event-identifier>
+ <event-date>2011-01-31</event-date>
+ <event-time>17:06:02</event-time>
+ <event-time-zone>EST</event-time-zone>
+ <event-description>Article ramass&#233; par Postes Canada</event-description>
+ <signatory-name />
+ <event-site>MONTREAL</event-site>
+ <event-province>QC</event-province>
+ <event-retail-location-id />
+ <event-retail-name />
+ </occurrence>
+ <occurrence>
+ <event-identifier>3000</event-identifier>
+ <event-date>2011-01-31</event-date>
+ <event-time>14:34:57</event-time>
+ <event-time-zone>EST</event-time-zone>
+ <event-description>Postes Canada a re&#231;u l'information concernant
+ la commande</event-description>
+ <signatory-name />
+ <event-site>LACHINE</event-site>
+ <event-province>QC</event-province>
+ <event-retail-location-id />
+ <event-retail-name />
+ </occurrence>
+ </significant-events>
+</tracking-detail>
+
+
+
View
247 test/remote/canada_post_pws_platform_test.rb
@@ -0,0 +1,247 @@
+# encoding: utf-8
+
+require 'test_helper'
+
+# All remote tests require Canada Post development environment credentials
+class CanadaPostPWSPlatformTest < Test::Unit::TestCase
+
+ def setup
+
+ @login = fixtures(:canada_post_pws_production)
+
+ # 100 grams, 93 cm long, 10 cm diameter, cylinders have different volume calculations
+ # @pkg1 = Package.new(1000, [93,10], :value => 10.00)
+ @pkg1