From 0ad0aa2c3fc067aa71bbae5ba71882023787c523 Mon Sep 17 00:00:00 2001 From: Philip Arndt Date: Fri, 6 Jul 2012 15:47:18 +1200 Subject: [PATCH] Allow shipments to be created independently of labels. --- lib/fedex/request/base.rb | 1 + lib/fedex/request/label.rb | 88 ++-------------- lib/fedex/request/rate.rb | 2 +- lib/fedex/request/shipment.rb | 106 ++++++++++++++++++++ lib/fedex/shipment.rb | 8 ++ spec/lib/fedex/rate_spec.rb | 149 ++++++++++++++++++++++++++++ spec/lib/fedex/shipment_spec.rb | 171 ++++++-------------------------- 7 files changed, 308 insertions(+), 217 deletions(-) create mode 100644 lib/fedex/request/shipment.rb create mode 100644 spec/lib/fedex/rate_spec.rb diff --git a/lib/fedex/request/base.rb b/lib/fedex/request/base.rb index d4262e53..b5e8118f 100644 --- a/lib/fedex/request/base.rb +++ b/lib/fedex/request/base.rb @@ -48,6 +48,7 @@ def initialize(credentials, options={}) requires!(options, :shipper, :recipient, :packages, :service_type) @credentials = credentials @shipper, @recipient, @packages, @service_type, @customs_clearance, @debug = options[:shipper], options[:recipient], options[:packages], options[:service_type], options[:customs_clearance], options[:debug] + @debug = ENV['DEBUG'] == 'true' @shipping_options = options[:shipping_options] ||={} end diff --git a/lib/fedex/request/label.rb b/lib/fedex/request/label.rb index 331bd245..bec49950 100644 --- a/lib/fedex/request/label.rb +++ b/lib/fedex/request/label.rb @@ -1,95 +1,27 @@ require 'fedex/request/base' require 'fedex/label' +require 'fedex/request/shipment' require 'fileutils' module Fedex module Request - class Label < Base - #attr_reader :response_details - + class Label < Shipment def initialize(credentials, options={}) super(credentials, options) @filename = options[:filename] - @label_specification = options[:label_specification] || {} - end - - # Sends post request to Fedex web service and parse the response. - # A label file is created with the label at the specified location. - # The parsed Fedex response is available in #response_details - # e.g. response_details[:completed_shipment_detail][:completed_package_details][:tracking_ids][:tracking_number] - def process_request - api_response = self.class.post(api_url, :body => build_xml) - puts api_response if @debug == true - response = parse_response(api_response) - if success?(response) - # @response_details = response[:process_shipment_reply] - # package_details = response[:process_shipment_reply][:completed_shipment_detail][:completed_package_details] - # label_details = package_details[:label] - # label_details[:format] = @label_specification[:image_type] || 'PDF' - # label_details[:tracking_number] = package_details[:tracking_ids][:tracking_number] - # label_details[:file_name] = @filename - format = @label_specification[:image_type] || 'PDF' - label_details = response.merge!({:format => format, :file_name => @filename}) - Fedex::Label.new(label_details) - else - error_message = if response[:process_shipment_reply] - [response[:process_shipment_reply][:notifications]].flatten.first[:message] - else - api_response["Fault"]["detail"]["fault"]["reason"] - end rescue $1 - raise RateError, error_message - end end private - # Add information for shipments - def add_requested_shipment(xml) - xml.RequestedShipment{ - xml.ShipTimestamp Time.now.utc.iso8601(2) - xml.DropoffType @shipping_options[:drop_off_type] || "REGULAR_PICKUP" - xml.ServiceType service_type - xml.PackagingType @shipping_options[:packaging_type] || "YOUR_PACKAGING" - add_shipper(xml) - add_recipient(xml) - add_shipping_charges_payment(xml) - add_customs_clearance(xml) if @customs_clearance - add_label_specification(xml) - xml.RateRequestTypes "ACCOUNT" - add_packages(xml) - } - end - - # Add the label specification - def add_label_specification(xml) - xml.LabelSpecification { - xml.LabelFormatType @label_specification[:label_format_type] || 'COMMON2D' - xml.ImageType @label_specification[:image_type] || 'PDF' - xml.LabelStockType @label_specification[:label_stock_type] || 'PAPER_LETTER' - } - end - - # Build xml Fedex Web Service request - def build_xml - builder = Nokogiri::XML::Builder.new do |xml| - xml.ProcessShipmentRequest(:xmlns => "http://fedex.com/ws/ship/v10"){ - add_web_authentication_detail(xml) - add_client_detail(xml) - add_version(xml) - add_requested_shipment(xml) - } - end - builder.doc.root.to_xml - end - - def service_id - 'ship' - end + def success_response(api_response, response) + super + format = @label_specification[:image_type] + label_details = response.merge!({ + :format => format, + :file_name => @filename + }) - # Successful request - def success?(response) - response[:process_shipment_reply] && - %w{SUCCESS WARNING NOTE}.include?(response[:process_shipment_reply][:highest_severity]) + Fedex::Label.new label_details end end diff --git a/lib/fedex/request/rate.rb b/lib/fedex/request/rate.rb index 5c22105a..3192bbdb 100644 --- a/lib/fedex/request/rate.rb +++ b/lib/fedex/request/rate.rb @@ -6,7 +6,7 @@ class Rate < Base # Sends post request to Fedex web service and parse the response, a Rate object is created if the response is successful def process_request api_response = self.class.post(api_url, :body => build_xml) - puts api_response if @debug == true + puts api_response if @debug response = parse_response(api_response) if success?(response) rate_details = [response[:rate_reply][:rate_reply_details][:rated_shipment_details]].flatten.first[:shipment_rate_detail] diff --git a/lib/fedex/request/shipment.rb b/lib/fedex/request/shipment.rb new file mode 100644 index 00000000..f8290ae8 --- /dev/null +++ b/lib/fedex/request/shipment.rb @@ -0,0 +1,106 @@ +require 'fedex/request/base' + +module Fedex + module Request + class Shipment < Base + attr_reader :response_details + + def initialize(credentials, options={}) + super + requires! options + # Label specification is required even if we're not using it. + @label_specification = { + :label_format_type => 'COMMON2D', + :image_type => 'PDF', + :label_stock_type => 'PAPER_LETTER' + } + @label_specification.merge! options[:label_specification] if options[:label_specification] + end + + # Sends post request to Fedex web service and parse the response. + # A label file is created with the label at the specified location. + # The parsed Fedex response is available in #response_details + # e.g. response_details[:completed_shipment_detail][:completed_package_details][:tracking_ids][:tracking_number] + def process_request + api_response = self.class.post api_url, :body => build_xml + puts api_response if @debug + response = parse_response(api_response) + if success?(response) + success_response(api_response, response) + else + failure_response(api_response, response) + end + end + + private + + # Add information for shipments + def add_requested_shipment(xml) + xml.RequestedShipment{ + xml.ShipTimestamp Time.now.utc.iso8601(2) + xml.DropoffType @shipping_options[:drop_off_type] ||= "REGULAR_PICKUP" + xml.ServiceType service_type + xml.PackagingType @shipping_options[:packaging_type] ||= "YOUR_PACKAGING" + add_shipper(xml) + add_recipient(xml) + add_shipping_charges_payment(xml) + add_customs_clearance(xml) if @customs_clearance + add_custom_components(xml) + xml.RateRequestTypes "ACCOUNT" + add_packages(xml) + } + end + + # Hook that can be used to add custom parts. + def add_custom_components(xml) + add_label_specification xml + end + + # Add the label specification + def add_label_specification(xml) + xml.LabelSpecification { + hash_to_xml(xml, @label_specification) + } + end + + # Callback used after a failed shipment response. + def failure_response(api_response, response) + error_message = if response[:process_shipment_reply] + [response[:process_shipment_reply][:notifications]].flatten.first[:message] + else + api_response["Fault"]["detail"]["fault"]["reason"] + end rescue $1 + raise RateError, error_message + end + + # Callback used after a successful shipment response. + def success_response(api_response, response) + @response_details = response[:process_shipment_reply] + end + + # Build xml Fedex Web Service request + def build_xml + builder = Nokogiri::XML::Builder.new do |xml| + xml.ProcessShipmentRequest(:xmlns => "http://fedex.com/ws/ship/v10"){ + add_web_authentication_detail(xml) + add_client_detail(xml) + add_version(xml) + add_requested_shipment(xml) + } + end + builder.doc.root.to_xml + end + + def service_id + 'ship' + end + + # Successful request + def success?(response) + response[:process_shipment_reply] && + %w{SUCCESS WARNING NOTE}.include?(response[:process_shipment_reply][:highest_severity]) + end + + end + end +end diff --git a/lib/fedex/shipment.rb b/lib/fedex/shipment.rb index fbf0dda7..60079baa 100644 --- a/lib/fedex/shipment.rb +++ b/lib/fedex/shipment.rb @@ -36,5 +36,13 @@ def rate(options = {}) Request::Rate.new(@credentials, options).process_request end + # @param [Hash] shipper, A hash containing the shipper information + # @param [Hash] recipient, A hash containing the recipient information + # @param [Array] packages, An arrary including a hash for each package being shipped + # @param [String] service_type, A valid fedex service type, to view a complete list of services Fedex::Shipment::SERVICE_TYPES + def ship(options = {}) + Request::Shipment.new(@credentials, options).process_request + end + end end diff --git a/spec/lib/fedex/rate_spec.rb b/spec/lib/fedex/rate_spec.rb new file mode 100644 index 00000000..fc70e53a --- /dev/null +++ b/spec/lib/fedex/rate_spec.rb @@ -0,0 +1,149 @@ +require 'spec_helper' + +module Fedex + describe Shipment do + context "missing required parameters" do + it "should raise Rate exception" do + lambda{ Shipment.new}.should raise_error(RateError) + end + end + + context "required parameters present" do + subject { Shipment.new(fedex_credentials) } + it "should create a valid instance" do + subject.should be_an_instance_of(Shipment) + end + end + + describe "rate service" do + let(:fedex) { Shipment.new(fedex_credentials) } + let(:shipper) do + {:name => "Sender", :company => "Company", :phone_number => "555-555-5555", :address => "Main Street", :city => "Harrison", :state => "AR", :postal_code => "72601", :country_code => "US"} + end + let(:recipient) do + {:name => "Recipient", :company => "Company", :phone_number => "555-555-5555", :address => "Main Street", :city => "Frankin Park", :state => "IL", :postal_code => "60131", :country_code => "US", :residential => true } + end + let(:packages) do + [ + { + :weight => {:units => "LB", :value => 2}, + :dimensions => {:length => 10, :width => 5, :height => 4, :units => "IN" } + }, + { + :weight => {:units => "LB", :value => 6}, + :dimensions => {:length => 5, :width => 5, :height => 4, :units => "IN" } + } + ] + end + let(:shipping_options) do + { :packaging_type => "YOUR_PACKAGING", :drop_off_type => "REGULAR_PICKUP" } + end + + context "domestic shipment", :vcr do + it "should return a rate" do + rate = fedex.rate({:shipper => shipper, :recipient => recipient, :packages => packages, :service_type => "FEDEX_GROUND"}) + rate.should be_an_instance_of(Rate) + end + end + + context "canadian shipment", :vcr do + it "should return a rate" do + canadian_recipient = {:name => "Recipient", :company => "Company", :phone_number => "555-555-5555", :address=>"Address Line 1", :city => "Richmond", :state => "BC", :postal_code => "V7C4V4", :country_code => "CA", :residential => "true" } + rate = fedex.rate({:shipper => shipper, :recipient => canadian_recipient, :packages => packages, :service_type => "FEDEX_GROUND"}) + rate.should be_an_instance_of(Rate) + end + end + + context "canadian shipment including customs", :vcr do + it "should return a rate including international fees" do + canadian_recipient = {:name => "Recipient", :company => "Company", :phone_number => "555-555-5555", :address=>"Address Line 1", :city => "Richmond", :state => "BC", :postal_code => "V7C4V4", :country_code => "CA", :residential => "true" } + broker = { + :account_number => "510087143", + :tins => { :tin_type => "BUSINESS_NATIONAL", + :number => "431870271", + :usage => "Usage" }, + :contact => { :contact_id => "1", + :person_name => "Broker Name", + :title => "Broker", + :company_name => "Broker One", + :phone_number => "555-555-5555", + :phone_extension => "555-555-5555", + :pager_number => "555", + :fax_number=> "555-555-5555", + :e_mail_address => "contact@me.com" }, + :address => { :street_lines => "Main Street", + :city => "Franklin Park", + :state_or_province_code => 'IL', + :postal_code => '60131', + :urbanization_code => '123', + :country_code => 'US', + :residential => 'false' } + } + + clearance_brokerage = "BROKER_INCLUSIVE" + + importer_of_record= { + :account_number => "22222", + :tins => { :tin_type => "BUSINESS_NATIONAL", + :number => "22222", + :usage => "Usage" }, + :contact => { :contact_id => "1", + :person_name => "Importer Name", + :title => "Importer", + :company_name => "Importer One", + :phone_number => "555-555-5555", + :phone_extension => "555-555-5555", + :pager_number => "555", + :fax_number=> "555-555-5555", + :e_mail_address => "contact@me.com" }, + :address => { :street_lines => "Main Street", + :city => "Chicago", + :state_or_province_code => 'IL', + :postal_code => '60611', + :urbanization_code => '2308', + :country_code => 'US', + :residential => 'false' } + } + + recipient_customs_id = { :type => 'COMPANY', + :value => '1254587' } + + + duties_payment = { :payment_type => "SENDER", + :payor => { :account_number => "510087143", + :country_code => "US" } } + + customs_value = { :currency => "USD", + :amount => "200" } + commodities = [{ + :name => "Cotton Coat", + :number_of_pieces => "2", + :description => "Cotton Coat", + :country_of_manufacture => "US", + :harmonized_code => "6103320000", + :weight => {:units => "LB", :value => "2"}, + :quantity => "3", + :unit_price => {:currency => "USD", :amount => "50" }, + :customs_value => {:currency => "USD", :amount => "150" } + }, + { + :name => "Poster", + :number_of_pieces => "1", + :description => "Paper Poster", + :country_of_manufacture => "US", + :harmonized_code => "4817100000", + :weight => {:units => "LB", :value => "0.2"}, + :quantity => "3", + :unit_price => {:currency => "USD", :amount => "50" }, + :customs_value => {:currency => "USD", :amount => "150" } + } + ] + + customs_clearance = { :broker => broker, :clearance_brokerage => clearance_brokerage, :importer_of_record => importer_of_record, :recipient_customs_id => recipient_customs_id, :duties_payment => duties_payment, :commodities => commodities } + rate = fedex.rate({:shipper => shipper, :recipient => canadian_recipient, :packages => packages, :service_type => "FEDEX_GROUND", :customs_clearance => customs_clearance}) + rate.should be_an_instance_of(Rate) + end + end + end + end +end \ No newline at end of file diff --git a/spec/lib/fedex/shipment_spec.rb b/spec/lib/fedex/shipment_spec.rb index fc70e53a..7969b555 100644 --- a/spec/lib/fedex/shipment_spec.rb +++ b/spec/lib/fedex/shipment_spec.rb @@ -1,149 +1,44 @@ require 'spec_helper' +require 'fedex/shipment' -module Fedex - describe Shipment do - context "missing required parameters" do - it "should raise Rate exception" do - lambda{ Shipment.new}.should raise_error(RateError) - end +describe Fedex::Request::Shipment do + describe "ship service" do + let(:fedex) { Fedex::Shipment.new(fedex_credentials) } + let(:shipper) do + {:name => "Sender", :company => "Company", :phone_number => "555-555-5555", :address => "Main Street", :city => "Harrison", :state => "AR", :postal_code => "72601", :country_code => "US"} end - - context "required parameters present" do - subject { Shipment.new(fedex_credentials) } - it "should create a valid instance" do - subject.should be_an_instance_of(Shipment) - end + let(:recipient) do + {:name => "Recipient", :company => "Company", :phone_number => "555-555-5555", :address => "Main Street", :city => "Frankin Park", :state => "IL", :postal_code => "60131", :country_code => "US", :residential => true } + end + let(:packages) do + [ + { + :weight => {:units => "LB", :value => 2}, + :dimensions => {:length => 10, :width => 5, :height => 4, :units => "IN" } + } + ] + end + let(:shipping_options) do + { :packaging_type => "YOUR_PACKAGING", :drop_off_type => "REGULAR_PICKUP" } end - describe "rate service" do - let(:fedex) { Shipment.new(fedex_credentials) } - let(:shipper) do - {:name => "Sender", :company => "Company", :phone_number => "555-555-5555", :address => "Main Street", :city => "Harrison", :state => "AR", :postal_code => "72601", :country_code => "US"} - end - let(:recipient) do - {:name => "Recipient", :company => "Company", :phone_number => "555-555-5555", :address => "Main Street", :city => "Frankin Park", :state => "IL", :postal_code => "60131", :country_code => "US", :residential => true } - end - let(:packages) do - [ - { - :weight => {:units => "LB", :value => 2}, - :dimensions => {:length => 10, :width => 5, :height => 4, :units => "IN" } - }, - { - :weight => {:units => "LB", :value => 6}, - :dimensions => {:length => 5, :width => 5, :height => 4, :units => "IN" } - } - ] - end - let(:shipping_options) do - { :packaging_type => "YOUR_PACKAGING", :drop_off_type => "REGULAR_PICKUP" } - end - - context "domestic shipment", :vcr do - it "should return a rate" do - rate = fedex.rate({:shipper => shipper, :recipient => recipient, :packages => packages, :service_type => "FEDEX_GROUND"}) - rate.should be_an_instance_of(Rate) - end - end - - context "canadian shipment", :vcr do - it "should return a rate" do - canadian_recipient = {:name => "Recipient", :company => "Company", :phone_number => "555-555-5555", :address=>"Address Line 1", :city => "Richmond", :state => "BC", :postal_code => "V7C4V4", :country_code => "CA", :residential => "true" } - rate = fedex.rate({:shipper => shipper, :recipient => canadian_recipient, :packages => packages, :service_type => "FEDEX_GROUND"}) - rate.should be_an_instance_of(Rate) - end + context "domestic shipment", :vcr do + let(:filename) { + require 'tmpdir' + File.join(Dir.tmpdir, "label#{rand(15000)}.pdf") + } + let(:options) do + {:shipper => shipper, :recipient => recipient, :packages => packages, :service_type => "FEDEX_GROUND", :filename => filename} end - context "canadian shipment including customs", :vcr do - it "should return a rate including international fees" do - canadian_recipient = {:name => "Recipient", :company => "Company", :phone_number => "555-555-5555", :address=>"Address Line 1", :city => "Richmond", :state => "BC", :postal_code => "V7C4V4", :country_code => "CA", :residential => "true" } - broker = { - :account_number => "510087143", - :tins => { :tin_type => "BUSINESS_NATIONAL", - :number => "431870271", - :usage => "Usage" }, - :contact => { :contact_id => "1", - :person_name => "Broker Name", - :title => "Broker", - :company_name => "Broker One", - :phone_number => "555-555-5555", - :phone_extension => "555-555-5555", - :pager_number => "555", - :fax_number=> "555-555-5555", - :e_mail_address => "contact@me.com" }, - :address => { :street_lines => "Main Street", - :city => "Franklin Park", - :state_or_province_code => 'IL', - :postal_code => '60131', - :urbanization_code => '123', - :country_code => 'US', - :residential => 'false' } - } - - clearance_brokerage = "BROKER_INCLUSIVE" - - importer_of_record= { - :account_number => "22222", - :tins => { :tin_type => "BUSINESS_NATIONAL", - :number => "22222", - :usage => "Usage" }, - :contact => { :contact_id => "1", - :person_name => "Importer Name", - :title => "Importer", - :company_name => "Importer One", - :phone_number => "555-555-5555", - :phone_extension => "555-555-5555", - :pager_number => "555", - :fax_number=> "555-555-5555", - :e_mail_address => "contact@me.com" }, - :address => { :street_lines => "Main Street", - :city => "Chicago", - :state_or_province_code => 'IL', - :postal_code => '60611', - :urbanization_code => '2308', - :country_code => 'US', - :residential => 'false' } - } - - recipient_customs_id = { :type => 'COMPANY', - :value => '1254587' } + it "succeeds" do + expect { + @shipment = fedex.ship(options) + }.to_not raise_error - - duties_payment = { :payment_type => "SENDER", - :payor => { :account_number => "510087143", - :country_code => "US" } } - - customs_value = { :currency => "USD", - :amount => "200" } - commodities = [{ - :name => "Cotton Coat", - :number_of_pieces => "2", - :description => "Cotton Coat", - :country_of_manufacture => "US", - :harmonized_code => "6103320000", - :weight => {:units => "LB", :value => "2"}, - :quantity => "3", - :unit_price => {:currency => "USD", :amount => "50" }, - :customs_value => {:currency => "USD", :amount => "150" } - }, - { - :name => "Poster", - :number_of_pieces => "1", - :description => "Paper Poster", - :country_of_manufacture => "US", - :harmonized_code => "4817100000", - :weight => {:units => "LB", :value => "0.2"}, - :quantity => "3", - :unit_price => {:currency => "USD", :amount => "50" }, - :customs_value => {:currency => "USD", :amount => "150" } - } - ] - - customs_clearance = { :broker => broker, :clearance_brokerage => clearance_brokerage, :importer_of_record => importer_of_record, :recipient_customs_id => recipient_customs_id, :duties_payment => duties_payment, :commodities => commodities } - rate = fedex.rate({:shipper => shipper, :recipient => canadian_recipient, :packages => packages, :service_type => "FEDEX_GROUND", :customs_clearance => customs_clearance}) - rate.should be_an_instance_of(Rate) - end + @shipment.class.should_not == Fedex::RateError end end + end -end \ No newline at end of file +end