diff --git a/.travis.yml b/.travis.yml index cc038d895..2aee8f0d8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,4 +24,5 @@ env: - ACTIVESHIPPING_CANADA_POST_PWS_CUSTOMER_NUMBER=2004381 - ACTIVESHIPPING_CANADA_POST_PWS_API_KEY=6e93d53968881714 - ACTIVESHIPPING_CANADA_POST_PWS_SECRET=0bfa9fcb9853d1f51ee57a + - ACTIVESHIPPING_CANADA_POST_PWS_CONTRACT=42708517 - ACTIVESHIPPING_USPS_LOGIN=677JADED7283 diff --git a/lib/active_shipping/carriers/canada_post_pws.rb b/lib/active_shipping/carriers/canada_post_pws.rb index 2718aecf7..dca29fc9e 100644 --- a/lib/active_shipping/carriers/canada_post_pws.rb +++ b/lib/active_shipping/carriers/canada_post_pws.rb @@ -33,6 +33,7 @@ class CanadaPostPWS < Carrier ENDPOINT = "https://soa-gw.canadapost.ca/" # production SHIPMENT_MIMETYPE = "application/vnd.cpc.ncshipment+xml" + CONTRACT_MIMETYPE = "application/vnd.cpc.shipment-v8+xml" RATE_MIMETYPE = "application/vnd.cpc.ship.rate+xml" TRACK_MIMETYPE = "application/vnd.cpc.track+xml" REGISTER_MIMETYPE = "application/vnd.cpc.registration+xml" @@ -107,6 +108,16 @@ def create_shipment(origin, destination, package, line_items = [], options = {}) CPPWSShippingResponse.new(false, "Missing Customer Number", {}, :carrier => @@name) end + def create_contract_shipment(origin, destination, package, line_items = [], options = {}, return_details = {}) + request_body = build_contract_shipment_request(origin, destination, package, line_items, options, return_details) + response = ssl_post(create_contract_shipment_url(options), request_body, headers(options, CONTRACT_MIMETYPE, CONTRACT_MIMETYPE)) + parse_contract_shipment_response(response) + rescue ActiveUtils::ResponseError, ActiveShipping::ResponseError => e + error_response(e.response.body, CPPWSShippingResponse) + rescue MissingCustomerNumberError + 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)) parse_shipment_response(response) @@ -358,6 +369,7 @@ def build_tracking_events(events) # :show_postage_rate # :cod, :cod_amount, :insurance, :insurance_amount, :signature_required, :pa18, :pa19, :hfp, :dns, :lad # + # def build_shipment_request(origin, destination, package, line_items = [], options = {}) builder = Nokogiri::XML::Builder.new do |xml| xml.public_send('non-contract-shipment', :xmlns => "http://www.canadapost.ca/ws/ncshipment") do @@ -378,10 +390,48 @@ def build_shipment_request(origin, destination, package, line_items = [], option builder.to_xml end + # return_details + # :service_code, :return_recipient {:name, :company, :address_details {:address_line_1...}} + + def build_contract_shipment_request(origin, destination, package, line_items = [], options = {}, return_details = {}) + builder = Nokogiri::XML::Builder.new do |xml| + xml.public_send('shipment', :xmlns => "http://www.canadapost.ca/ws/shipment-v8") do + shipment_node(xml, options) + xml.public_send('delivery-spec') do + shipment_service_code_node(xml, options) + shipment_sender_node(xml, origin, options) + shipment_destination_node(xml, destination, options) + shipment_options_node(xml, options) + shipment_parcel_node(xml, package) + shipment_notification_node(xml, options) + shipment_preferences_node(xml, options) + references_node(xml, options) # optional > user defined custom notes + shipment_customs_node(xml, destination, line_items, options) + settlement_node(xml, options) + # COD Remittance defaults to sender + end + shipment_return_node(xml, return_details, options) + end + end + builder.to_xml + end + + def shipment_node(xml, options) + xml.public_send('transmit-shipment', options[:transmit] || true) + xml.public_send('requested-shipping-point', options[:shipping_point]) + end + def shipment_service_code_node(xml, options) xml.public_send('service-code', options[:service]) end + def settlement_node(xml, options) + xml.public_send('settlement-info') do + xml.public_send('contract-id', options[:contract_number]) + xml.public_send('intended-method-of-payment', options[:payment_type] || 'Account' ) + end + end + def shipment_sender_node(xml, sender, options) location = location_from_hash(sender) xml.public_send('sender') do @@ -393,7 +443,7 @@ def shipment_sender_node(xml, sender, options) xml.public_send('address-line-2', location.address2_and_3) unless location.address2_and_3.blank? xml.public_send('city', location.city) xml.public_send('prov-state', location.province) - # xml.public_send('country-code', location.country_code) + xml.public_send('country-code', location.country_code) unless location.country_code.blank? xml.public_send('postal-zip-code', get_sanitized_postal_code(location)) end end @@ -473,6 +523,28 @@ def shipment_customs_node(xml, destination, line_items, options) end end + # return_details + # :service_code, :return_recipient {:name, :company, :address_details {:address_line_1...}} + + def shipment_return_node(xml, return_details, options) + return if return_details.blank? + location = location_from_hash(return_details[:return_recipient][:address_details]) + xml.public_send('return-spec') do + xml.public_send('service-code', return_details[:service_code] ) + xml.public_send('return-recipient') do + xml.public_send('name', return_details[:name]) if return_details[:name] + xml.public_send('company', return_details[:company]) if return_details[:company] + xml.public_send('address-details') do + xml.public_send('address-line-1', location.address1) + xml.public_send('address-line-2', location.address2_and_3) unless location.address2_and_3.blank? + xml.public_send('city', location.city) + xml.public_send('prov-state', location.province) unless location.province.blank? + xml.public_send('postal-zip-code', get_sanitized_postal_code(location)) + end + end + end + end + def shipment_parcel_node(xml, package, options = {}) weight = sanitize_weight_kg(package.kilograms.to_f) xml.public_send('parcel-characteristics') do @@ -484,7 +556,7 @@ def shipment_parcel_node(xml, package, options = {}) xml.public_send('width', '%.1f' % ((pkg_dim[1] * 10).round / 10.0)) if pkg_dim.size >= 2 xml.public_send('height', '%.1f' % ((pkg_dim[0] * 10).round / 10.0)) if pkg_dim.size >= 1 end - xml.public_send('document', false) + #xml.public_send('document', false) else xml.public_send('document', true) end @@ -494,6 +566,23 @@ def shipment_parcel_node(xml, package, options = {}) end end + def parse_contract_shipment_response(response) + doc = Nokogiri.XML(response) + doc.remove_namespaces! + raise ActiveShipping::ResponseError, "No Shipping" unless doc.at('shipment-info') + receipt_url = doc.root.at_xpath("links/link[@rel='receipt']")['href'] unless doc.root.at_xpath("links/link[@rel='receipt']").blank? + return_label_url = doc.root.at_xpath("links/link[@rel='returnLabel']")['href'] unless doc.root.at_xpath("links/link[@rel='returnLabel']").blank? + options = { + :shipping_id => doc.root.at('shipment-id').text, + :details_url => doc.root.at_xpath("links/link[@rel='details']")['href'], + :label_url => doc.root.at_xpath("links/link[@rel='label']")['href'], + :receipt_url => receipt_url, + :return_label_url => return_label_url + } + options[:tracking_number] = doc.root.at('tracking-pin').text if doc.root.at('tracking-pin') + CPPWSContractShippingResponse.new(true, "", {}, options) + end + def parse_shipment_response(response) doc = Nokogiri.XML(response) doc.remove_namespaces! @@ -505,7 +594,6 @@ def parse_shipment_response(response) :receipt_url => doc.root.at_xpath("links/link[@rel='receipt']")['href'], } options[:tracking_number] = doc.root.at('tracking-pin').text if doc.root.at('tracking-pin') - CPPWSShippingResponse.new(true, "", {}, options) end @@ -604,6 +692,15 @@ def create_shipment_url(options) end end + def create_contract_shipment_url(options) + raise MissingCustomerNumberError unless customer_number = options[:customer_number] + if @platform_id.present? + endpoint + "rs/#{customer_number}-#{@platform_id}/shipment" + else + endpoint + "rs/#{customer_number}/#{customer_number}/shipment" + end + end + def shipment_url(shipping_id, options = {}) raise MissingCustomerNumberError unless customer_number = options[:customer_number] if @platform_id.present? @@ -873,6 +970,19 @@ def initialize(success, message, params = {}, options = {}) end end + class CPPWSContractShippingResponse < ShippingResponse + include CPPWSErrorResponse + attr_reader :label_url, :details_url, :receipt_url, :return_label_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] + @return_label_url = options[:return_label_url] + end + end + class CPPWSRegisterResponse < Response include CPPWSErrorResponse attr_reader :token_id diff --git a/test/credentials.yml b/test/credentials.yml index 861689b83..dc706aecf 100644 --- a/test/credentials.yml +++ b/test/credentials.yml @@ -34,6 +34,7 @@ canada_post_pws: customer_number: <%= ENV['ACTIVESHIPPING_CANADA_POST_PWS_CUSTOMER_NUMBER'] %> api_key: <%= ENV['ACTIVESHIPPING_CANADA_POST_PWS_API_KEY'] %> secret: <%= ENV['ACTIVESHIPPING_CANADA_POST_PWS_SECRET'] %> + contract_number: <%= ENV['ACTIVESHIPPING_CANADA_POST_PWS_CONTRACT'] %> canada_post_pws_platform: platform_id: <%= ENV['ACTIVESHIPPING_CANADA_POST_PWS_PLATFORM_PLATFORM_ID'] %> diff --git a/test/remote/canada_post_pws_test.rb b/test/remote/canada_post_pws_test.rb index ac2d1fa72..c29b60d7d 100644 --- a/test/remote/canada_post_pws_test.rb +++ b/test/remote/canada_post_pws_test.rb @@ -2,6 +2,8 @@ class RemoteCanadaPostPWSTest < ActiveSupport::TestCase # All remote tests require Canada Post development environment credentials + # When using an account with a contract number you should add it to the opts hash + include ActiveShipping::Test::Credentials include ActiveShipping::Test::Fixtures @@ -14,7 +16,9 @@ def setup @line_item1 = line_item_fixture - @shipping_opts1 = { :dc => true, :cov => true, :cov_amount => 100.00, :aban => true } + @shipping_opts1 = {:cod => true, :dc => true, :cov => true, :cov_amount => 100.00, :aban => true } + + @shipping_opts2 = {so: true, cov: true, cov_amount: 999.99, transmit: true, shipping_point: 'R3W1S1'} @home_params = { :name => "John Smith", @@ -23,11 +27,22 @@ def setup :address1 => "123 Elm St.", :city => 'Ottawa', :province => 'ON', - :country => 'CA', :postal_code => 'K1P 1J1' } @home = Location.new(@home_params) + @contract_home_params = { + :name => "John Smith", + :company => "test", + :phone => "613-555-1212", + :address1 => "123 Elm St.", + :city => 'Ottawa', + :province => 'ON', + :country => 'CA', + :postal_code => 'K1P 1J1' + } + @contract_home = Location.new(@contract_home_params) + @dom_params = { :name => "John Smith Sr.", :company => "", @@ -77,6 +92,7 @@ def setup @cp.logger = Logger.new(StringIO.new) @customer_number = @login[:customer_number] + @contract_number = @login[:contract_number] @DEFAULT_RESPONSE = { :shipping_id => "406951321983787352", @@ -121,25 +137,51 @@ def test_tracking_when_no_tracking_info_raises_exception end def test_create_shipment - skip "Failing with 'Contract Number is a required field' after API change, skipping because no clue how to fix, might need different creds" + skip "contract number in credentials" if @contract_number opts = {:customer_number => @customer_number, :service => "DOM.XP"} response = @cp.create_shipment(@home_params, @dom_params, @pkg1, @line_item1, opts) assert_kind_of CPPWSShippingResponse, response - assert_match /\A\d{17}\z/, response.shipping_id + assert_match /\A\d{18}\z/, response.shipping_id assert_equal "123456789012", response.tracking_number - assert_match "https://ct.soa-gw.canadapost.ca/ers/artifact/", response.label_url + assert_match "https://ct.soa-gw.canadapost.ca/rs/artifact/", response.label_url assert_match @login[:api_key], response.label_url end def test_create_shipment_with_options - skip "Failing with 'Contract Number is a required field' after API change, skipping because no clue how to fix, might need different creds" + skip "contract number in credentials" if @contract_number opts = {:customer_number => @customer_number, :service => "USA.EP"}.merge(@shipping_opts1) response = @cp.create_shipment(@home_params, @dest_params, @pkg1, @line_item1, opts) - assert_kind_of CPPWSShippingResponse, response - assert_match /\A\d{17}\z/, response.shipping_id + assert_match /\A\d{18}\z/, response.shipping_id + assert_equal "123456789012", response.tracking_number + assert_match "https://ct.soa-gw.canadapost.ca/rs/artifact/", response.label_url + assert_match @login[:api_key], response.label_url + end + + def test_create_contract_shipment_with_options + skip "no contract number in credentials" unless @contract_number + opts = {:customer_number => @customer_number, :service => "DOM.XP", contract_number: @contract_number}.merge(@shipping_opts2) + response = @cp.create_contract_shipment(@contract_home_params, @dom_params, @pkg1, @line_item1, opts) + assert_kind_of CPPWSContractShippingResponse, response + assert_match /\A\d{18}\z/, response.shipping_id + assert_equal "123456789012", response.tracking_number + assert_match "https://ct.soa-gw.canadapost.ca/rs/artifact/", response.label_url + assert_match @login[:api_key], response.label_url + end + + def test_create_contract_shipment_with_return_label_and_options + skip "no contract number in credentials" unless @contract_number + + opts = {:customer_number => @customer_number, :service => "DOM.XP", contract_number: @contract_number}.merge(@shipping_opts2) + + return_details = {service_code: 'DOM.RP', return_recipient: { address_details: @contract_home_params } } + + response = @cp.create_contract_shipment(@contract_home_params, @dom_params, @pkg1, @line_item1, opts, return_details) + assert_kind_of CPPWSContractShippingResponse, response + assert_match /\A\d{18}\z/, response.shipping_id assert_equal "123456789012", response.tracking_number - assert_match "https://ct.soa-gw.canadapost.ca/ers/artifact/", response.label_url + assert_match "https://ct.soa-gw.canadapost.ca/rs/artifact/", response.label_url + assert_match "https://ct.soa-gw.canadapost.ca/rs/artifact/", response.return_label_url assert_match @login[:api_key], response.label_url end diff --git a/test/unit/carriers/canada_post_pws_shipping_test.rb b/test/unit/carriers/canada_post_pws_shipping_test.rb index 20db669f8..fd95a5d27 100644 --- a/test/unit/carriers/canada_post_pws_shipping_test.rb +++ b/test/unit/carriers/canada_post_pws_shipping_test.rb @@ -63,6 +63,7 @@ def setup :so => true, :pa18 => true} @default_options = {:customer_number => '123456'} + @default_contract_options = {:customer_number => '123456', :contract_number => '42708517'} @DEFAULT_RESPONSE = { :shipping_id => "406951321983787352", @@ -93,6 +94,30 @@ def test_build_shipment_request_for_domestic refute request.blank? end + def test_build_contract_shipment_request_for_domestic + options = @default_contract_options.dup + request = @cp.build_contract_shipment_request(@home_params, @dom_params, @pkg1, @line_item1, options) + refute request.blank? + end + + def test_build_contract_shipment_with_return_request_for_domestic + options = @default_contract_options.dup + return_details = {service_code: 'DOM.RP', return_recipient: { address_details: @home_params } } + + request = @cp.build_contract_shipment_request(@home_params, @dom_params, @pkg1, @line_item1, options, return_details) + refute request.blank? + + doc = Nokogiri.XML(request) + doc.remove_namespaces! + + assert root_node = doc.at('shipment') + assert delivery_spec = root_node.at('delivery-spec') + assert destination = delivery_spec.at('destination') + assert address_details = destination.at('address-details') + assert_equal 'CA', address_details.at('country-code').text + assert return_spec = root_node.at('return-spec') + end + def test_build_shipment_request_for_US options = @default_options.dup request = @cp.build_shipment_request(@home_params, @us_params, @pkg1, @line_item1, options)