Skip to content

Commit

Permalink
Allow shipments to be created independently of labels.
Browse files Browse the repository at this point in the history
  • Loading branch information
parndt committed Jul 6, 2012
1 parent 80d6e21 commit 0ad0aa2
Show file tree
Hide file tree
Showing 7 changed files with 308 additions and 217 deletions.
1 change: 1 addition & 0 deletions lib/fedex/request/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
88 changes: 10 additions & 78 deletions lib/fedex/request/label.rb
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion lib/fedex/request/rate.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
106 changes: 106 additions & 0 deletions lib/fedex/request/shipment.rb
Original file line number Diff line number Diff line change
@@ -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
8 changes: 8 additions & 0 deletions lib/fedex/shipment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
149 changes: 149 additions & 0 deletions spec/lib/fedex/rate_spec.rb
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit 0ad0aa2

Please sign in to comment.