Skip to content

Commit

Permalink
More tests and documentation cleanup
Browse files Browse the repository at this point in the history
  • Loading branch information
binarylogic committed Aug 6, 2009
1 parent cd1283f commit 050bef3
Show file tree
Hide file tree
Showing 9 changed files with 166 additions and 102 deletions.
4 changes: 2 additions & 2 deletions README.rdoc
Expand Up @@ -74,7 +74,7 @@ You will notice above we assign the result of the 'track' method to a variable c

== Interface integration

What's nice about having an object is that you can pass it around. Let's say you wanted to allow people to track their packages:
What's nice about having an object is that you can pass it around. Let's say you wanted to add simple FedEx tracking functionality to your app:

class TrackingController < ApplicationController
def new
Expand Down Expand Up @@ -107,7 +107,7 @@ Then your results:
.event
.name= event.name
.occured_at= event.occured_at.to_s(:long)
.location== #{event.city}, #{event.state} #{event.zip}, #{event.country}
.location== #{event.city}, #{event.state} #{event.postal_code}, #{event.country}
.residential= event.residential ? "Yes" : "No"

== Supported services
Expand Down
9 changes: 9 additions & 0 deletions lib/shippinglogic/fedex/attributes.rb
@@ -1,5 +1,6 @@
module Shippinglogic
class FedEx
# Adds in all of the reading / writing for the various serivce options.
module Attributes
def self.included(klass)
klass.class_eval do
Expand All @@ -9,6 +10,8 @@ def self.included(klass)
end

module ClassMethods
# Define an attribute for a class, makes adding options / attributes to the class
# much cleaner. See the Rates class for an example.
def attribute(name, type, options = {})
name = name.to_sym
options[:type] = type.to_sym
Expand All @@ -18,25 +21,30 @@ def attribute(name, type, options = {})
define_method("#{name}=") { |value| write_attribute(name, value) }
end

# A hash of all the attributes and their options
def attributes
@attributes ||= {}
end

# An array of the attribute names
def attribute_names
attributes.keys
end

# Returns the options specified when defining a specific attribute
def attribute_options(name)
attributes[name.to_sym]
end
end

module InstanceMethods
# A convenience so that you can set attributes while initializing an object
def initialize(base, attributes = {})
@attributes = {}
self.attributes = attributes
end

# Returns a hash of the various attribute values
def attributes
attributes = {}
attribute_names.each do |name|
Expand All @@ -45,6 +53,7 @@ def attributes
attributes
end

# Accepts a hash of attribute values and sets each attribute to those values
def attributes=(value)
return if value.blank?
value.each do |key, value|
Expand Down
146 changes: 67 additions & 79 deletions lib/shippinglogic/fedex/rates.rb
@@ -1,13 +1,62 @@
module Shippinglogic
class FedEx
# An interface to the rate services provided by FedEx.
# An interface to the rate services provided by FedEx. Allows you to get an array of rates from fedex for a shipment,
# or a single rate for a specific service.
#
# == Accessor methods / options
#
# * <tt>shipper_streets</tt> - street part of the address, separate multiple streets with a new line, dont include blank lines.
# * <tt>shipper_city</tt> - city part of the address.
# * <tt>shipper_state_</tt> - state part of the address, use state abreviations.
# * <tt>shipper_postal_code</tt> - postal code part of the address. Ex: zip for the US.
# * <tt>shipper_country</tt> - country code part of the address, use abbreviations, ex: 'US'
# * <tt>shipper_residential</tt> - a boolean value representing if the address is redential or not (default: false)
# * <tt>recipient_streets</tt> - street part of the address, separate multiple streets with a new line, dont include blank lines.
# * <tt>recipient_city</tt> - city part of the address.
# * <tt>recipient_state</tt> - state part of the address, use state abreviations.
# * <tt>recipient_postal_code</tt> - postal code part of the address. Ex: zip for the US.
# * <tt>recipient_country</tt> - country code part of the address, use abbreviations, ex: 'US'
# * <tt>recipient_residential</tt> - a boolean value representing if the address is redential or not (default: false)
# * <tt>service_type</tt> - one of SERVICE_TYPES, this is optional, leave this blank if you want a list of all
# available services. (default: nil)
# * <tt>packaging_type</tt> - one of PACKAGE_TYPES. (default: YOUR_PACKAGING)
# * <tt>packages</tt> - an array of packages included in the shipment. This should be an array of hashes with the following keys:
# * <tt>:weight</tt> - the weight
# * <tt>:weight_units</tt> - either LB or KG. (default: LB)
# * <tt>:length</tt> - the length.
# * <tt>:width</tt> - the width.
# * <tt>:height</tt> - the height.
# * <tt>:dimension_units</tt> - either IN or CM. (default: IN)
# * <tt>ship_time</tt> - a Time object representing when you want to ship the package. (default: Time.now)
# * <tt>dropoff_type</tt> - one of DROP_OFF_TYPES. (default: REGULAR_PICKUP)
# * <tt>include_transit_times</tt> - whether or not to include estimated transit times. (default: true)
# * <tt>delivery_deadline</tt> - whether or not to include estimated transit times. (default: true)
# * <tt>special_services_requested</tt> - any exceptions or special services FedEx needs to be aware of, this should be
# one or more of SPECIAL_SERVICES. (default: nil)
# * <tt>currency_type</tt> - the type of currency. (default: nil, because FedEx will default to your account preferences)
# * <tt>rate_request_types</tt> - one or more of RATE_REQUEST_TYPES. (default: ACCOUNT)
# * <tt>insured_value</tt> - the value you want to insure, if any. (default: nil)
# * <tt>payment_type</tt> - one of PAYMENT_TYPES. (default: SENDER)
# * <tt>payor_account_number</tt> - if the account paying for this ship is different than the account you specified then
# you can specify that here. (default: nil)
# * <tt>payor_country</tt> - the country code for the account number. (default: US)
#
# === Simple Example
#
# Here is a very simple example. Mix and match the options above to get more accurate rates:
#
# fedex = Shippinglogic::FedEx.new(key, password, account, meter)
# fedex.rates(
# :shipper_postal_code => "10007",
# :shipper_country => "US",
# :recipient_postal_code => "75201",
# :recipient_country_code => "US"
# :packages => [{:weight => 24, :length => 12, :width => 12, :height => 12}]
# )
class Rates < Service
# Each rate result is an object of this class
class Rate; attr_accessor :name, :type, :saturday, :deadline, :cost, :currency; end

include HTTParty
include Request
include Response

VERSION = {:major => 6, :intermediate => 0, :minor => 0}
DROP_OFF_TYPES = ["REGULAR_PICKUP", "REQUEST_COURIER", "DROP_BOX", "BUSINESS_SERVICE_CENTER", "STATION"]
PACKAGE_TYPES = ["FEDEX_ENVELOPE", "FEDEX_PAK", "FEDEX_BOX", "FEDEX_TUBE", "FEDEX_10KG_BOX", "FEDEX_25KG_BOX", "YOUR_PACKAGING"]
Expand All @@ -30,14 +79,14 @@ class Rate; attr_accessor :name, :type, :saturday, :deadline, :cost, :currency;
attribute :shipper_streets, :string
attribute :shipper_city, :string
attribute :shipper_state, :string
attribute :shipper_zip, :string
attribute :shipper_postal_code, :string
attribute :shipper_country, :string
attribute :shipper_residential, :boolean, :default => false

attribute :recipient_streets, :string
attribute :recipient_city, :string
attribute :recipient_state, :string
attribute :recipient_zip, :string
attribute :recipient_postal_code, :string
attribute :recipient_country, :string
attribute :recipient_residential, :boolean, :default => false

Expand All @@ -54,78 +103,9 @@ class Rate; attr_accessor :name, :type, :saturday, :deadline, :cost, :currency;
attribute :insured_value, :big_decimal
attribute :payment_type, :string, :default => "SENDER"
attribute :payor_account_number, :string
attribute :payor_country_code, :string
attribute :payor_country, :string, :default => "US"

private
# This method can return all of the available services with their rates, transit times, etc. It can also
# return information on a specific fedex service.
#
# === Options
#
# * <tt>:shipper</tt> - a hash of address information.
# * <tt>:street_lines</tt> - street part of the address, separate multiple streets with a new line, dont include blank lines.
# * <tt>:city</tt> - city part of the address.
# * <tt>:state_or_province_code</tt> - state part of the address, use state abreviations.
# * <tt>:postal_code</tt> - post code part of the address, zip for the US.
# * <tt>:country_code</tt> - country code part of the address, use abbreviations, ex: 'US'
# * <tt>:residential</tt> - a boolean value representing if the address is redential or not (default: nil)
# * <tt>:recipient</tt> - a hash of address information.
# * <tt>:street_lines</tt> - street part of the address, separate multiple streets with a new line, dont include blank lines.
# * <tt>:city</tt> - city part of the address.
# * <tt>:state_or_province_code</tt> - state part of the address, use state abreviations.
# * <tt>:postal_code</tt> - post code part of the address, zip for the US.
# * <tt>:country_code</tt> - country code part of the address, use abbreviations, ex: 'US'
# * <tt>:residential</tt> - a boolean value representing if the address is redential or not (default: nil)
# * <tt>:service_type</tt> - one of SERVICE_TYPES, this is optional, leave this blank if you want a list of all
# available services. (default: nil)
# * <tt>:packaging_type</tt> - one of PACKAGE_TYPES. (default: YOUR_PACKAGING)
# * <tt>:packages</tt> - an array of packages included in the shipment. This is optional and should default to whatever your packing
# type default is in your FedEx account. I am also fairly confident this is only required when using YOUR_PACKAGING as your
# packaging type. This should be an array of hashes with the following structure:
# * <tt>:weight</tt> - a hash of details about a single package weight
# * <tt>:units</tt> - either LB or KG. (default: LB)
# * <tt>:value</tt> - the weight
# * <tt>:dimensions</tt> - a hash of details about a single package dimensions
# * <tt>:units</tt> - either IN or CM. (default: IN)
# * <tt>:length</tt> - the length
# * <tt>:width</tt> - the width
# * <tt>:height</tt> - the height
# * <tt>:ship_time</tt> - a Time object representing when you want to ship the package. (default: Time.now)
# * <tt>:dropoff_type</tt> - one of DROP_OFF_TYPES. (default: REGULAR_PICKUP)
# * <tt>:include_transit_times</tt> - whether or not to include estimated transit times. (default: true)
# * <tt>:delivery_deadline</tt> - whether or not to include estimated transit times. (default: true)
# * <tt>:special_services_requested</tt> - any exceptions or special services FedEx needs to be aware of, this should be
# one or more of SPECIAL_SERVICES. (default: nil)
# * <tt>:currency_type</tt> - the type of currency. (default: nil, because FedEx will default to your account preferences)
# * <tt>:rate_request_types</tt> - one or more of RATE_REQUEST_TYPES. (default: ACCOUNT)
# * <tt>:insured_value</tt> - the value you want to insure, if any. (default: nil)
# * <tt>:payment_type</tt> - one of PAYMENT_TYPES. (default: SENDER)
# * <tt>:payor</tt> - this is optional, if the person paying for the shipment is different than the account you specify
# then you can specify that information here. This should be a hash with the following elements. (default: nil)
# * <tt>:account_number</tt> - the FedEx account number of the company paying for this shipment.
# * <tt>:country_code</tt> - the country code for the account number. Ex: 'US'
#
# === Simple Example
#
# Here is a very simple example. Mix and match the options above to get more accurate rates:
#
# fedex = Shippinglogic::FedEx.new(key, password, account, meter)
# fedex.available_services(
# :shipper => {
# :postal_code => "10007",
# :country_code => "US"
# },
# :recipient => {
# :postal_code => "75201",
# :country_code => "US"
# },
# :packages => [
# {
# :weight => {:value => 24},
# :dimensions => {:length => 12, :width => 12, :height => 12}
# }
# ]
# )
def target
@target ||= parse_rate_response(request(build_rate_request))
end
Expand All @@ -146,7 +126,15 @@ def build_rate_request
b.TotalInsuredValue insured_value if insured_value
b.Shipper { build_address(b, :shipper) }
b.Recipient { build_address(b, :recipient) }
b.ShippingChargesPayment { b.PaymentType payment_type }
b.ShippingChargesPayment do
b.PaymentType payment_type
if payor_account_number && payor_country_code
b.Payor do
b.AccountNumber payor_account_number
b.CountryCode payor_country
end
end
end
b.RateRequestTypes rate_request_types.join(",")
b.PackageCount packages.size
b.PackageDetail "INDIVIDUAL_PACKAGES"
Expand All @@ -161,7 +149,7 @@ def build_address(b, type)
b.StreetLines send("#{type}_streets") if send("#{type}_streets")
b.City send("#{type}_city") if send("#{type}_city")
b.StateOrProvinceCode send("#{type}_state") if send("#{type}_state")
b.PostalCode send("#{type}_zip") if send("#{type}_zip")
b.PostalCode send("#{type}_postal_code") if send("#{type}_postal_code")
b.CountryCode send("#{type}_country") if send("#{type}_country")
b.Residential send("#{type}_residential")
end
Expand Down
34 changes: 22 additions & 12 deletions lib/shippinglogic/fedex/track.rb
@@ -1,23 +1,33 @@
module Shippinglogic
class FedEx
# An interface to the track services provided by FedEx. See the track method for more info. Alternatively, you can
# read up on the track services documentation on the FedEx website.
# An interface to the track services provided by FedEx. Allows you to get an array of events for a specific
# tracking number.
#
# == Accessor methods / options
#
# * <tt>tracking_number</tt> - the tracking number
#
# === Simple Example
#
# Here is a very simple example:
#
# fedex = Shippinglogic::FedEx.new(key, password, account, meter)
# fedex.track(:tracking_number => "my number")
#
# === Note
# FedEx does support locating packages through means other than a tracking number.
# These are not supported and probably won't be until someone needs them. It should
# be fairly simple to add, but I could not think of a reason why anyone would want to track
# a package with anything other than a tracking number.
class Track < Service
class Event; attr_accessor :name, :type, :occured_at, :city, :state, :zip, :country, :residential; end
# Each tracking result is an object of this class
class Event; attr_accessor :name, :type, :occured_at, :city, :state, :postal_code, :country, :residential; end

VERSION = {:major => 3, :intermediate => 0, :minor => 0}

attribute :tracking_number, :string

private
# This returns the various tracking information for a specific tracking number.
#
# === Note
#
# FedEx does support locating packages through means other than a tracking number.
# These are not supported and probably won't be until someone needs them. It should
# be fairly simple to add, but I could not think of a reason why anyone would want to track
# a package with anything other than a tracking number.
def target
@target ||= parse_track_response(request(build_track_request))
end
Expand Down Expand Up @@ -45,7 +55,7 @@ def parse_track_response(response)
event.occured_at = Time.parse(details[:timestamp])
event.city = details[:address][:city]
event.state = details[:address][:state_or_province_code]
event.zip = details[:address][:postal_code]
event.postal_code = details[:address][:postal_code]
event.country = details[:address][:country_code]
event.residential = details[:address][:residential] == "true"
event
Expand Down
18 changes: 12 additions & 6 deletions lib/shippinglogic/fedex/validation.rb
@@ -1,16 +1,22 @@
module Shippinglogic
class FedEx
# This module is more for application integration, so you can do something like:
#
# tracking = fedex.tracking
# if tracking.valid?
# # render a successful response
# else
# # do something with the errors: fedex.errors
# end
module Validation
def self.included(klass)
klass.class_eval do
attr_accessor :errors
end
end

# Just an array of errors that were encounted if valid? returns false.
def errors
@errors ||= []
end

# Allows you to determine if the request is valid or not. All validation is delegated to the FedEx
# services, so what this does is make a call to FedEx and rescue any errors, then it puts those
# error into the 'errors' array.
def valid?
begin
target
Expand Down
41 changes: 41 additions & 0 deletions spec/fedex/attributes_spec.rb
@@ -0,0 +1,41 @@
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')

describe "FedEx Attributes" do
it "should allow setting attributes upon initialization" do
tracking = new_fedex.track(:tracking_number => fedex_tracking_number)
tracking.tracking_number.should == fedex_tracking_number
end

it "should allow setting attributes individually" do
tracking = new_fedex.track
tracking.tracking_number = fedex_tracking_number
tracking.tracking_number.should == fedex_tracking_number
end

it "should allow setting attributes with a hash" do
tracking = new_fedex.track
tracking.attributes = {:tracking_number => fedex_tracking_number}
tracking.tracking_number.should == fedex_tracking_number
end

it "should allow reading attributes" do
tracking = new_fedex.track
tracking.attributes = {:tracking_number => fedex_tracking_number}
tracking.attributes.should == {:tracking_number => fedex_tracking_number}
end

it "should implement defaults" do
rates = new_fedex.rates
rates.shipper_residential.should == false
end

it "should use blank array as defaults for arrays" do
rates = new_fedex.rates
rates.packages.should == []
end

it "should call procs during run time if a default is a proc" do
rates = new_fedex.rates
rates.ship_time.to_s.should == Time.now.to_s
end
end
2 changes: 1 addition & 1 deletion spec/fedex/track_spec.rb
Expand Up @@ -13,7 +13,7 @@
event.occured_at.should == Time.parse("Mon Dec 08 10:43:37 -0500 2008")
event.city.should == "Sacramento"
event.state.should == "CA"
event.zip.should == "95817"
event.postal_code.should == "95817"
event.country.should == "US"
event.residential.should == false
end
Expand Down

0 comments on commit 050bef3

Please sign in to comment.