Permalink
Browse files

fix conflicts

  • Loading branch information...
2 parents 34aca2d + 64c621f commit b4518ed9f92d75b0fc795c5056085cd0d0f8c1be @binarylogic committed Aug 21, 2010
Showing with 1,625 additions and 397 deletions.
  1. +1 −1 .gitignore
  2. +2 −1 Rakefile
  3. +1 −0 init.rb
  4. +3 −3 lib/shippinglogic.rb
  5. +121 −0 lib/shippinglogic/attributes.rb
  6. +13 −0 lib/shippinglogic/error.rb
  7. +8 −5 lib/shippinglogic/fedex.rb
  8. +0 −119 lib/shippinglogic/fedex/attributes.rb
  9. +2 −12 lib/shippinglogic/fedex/error.rb
  10. +0 −21 lib/shippinglogic/fedex/proxy.rb
  11. +4 −4 lib/shippinglogic/fedex/rate.rb
  12. +3 −1 lib/shippinglogic/fedex/request.rb
  13. +3 −1 lib/shippinglogic/fedex/response.rb
  14. +1 −28 lib/shippinglogic/fedex/service.rb
  15. +4 −2 lib/shippinglogic/fedex/ship.rb
  16. +3 −1 lib/shippinglogic/fedex/signature.rb
  17. +0 −32 lib/shippinglogic/fedex/validation.rb
  18. +19 −0 lib/shippinglogic/proxy.rb
  19. +42 −0 lib/shippinglogic/service.rb
  20. +83 −0 lib/shippinglogic/ups.rb
  21. +52 −0 lib/shippinglogic/ups/cancel.rb
  22. +56 −0 lib/shippinglogic/ups/enumerations.rb
  23. +45 −0 lib/shippinglogic/ups/error.rb
  24. +50 −0 lib/shippinglogic/ups/label.rb
  25. +228 −0 lib/shippinglogic/ups/rate.rb
  26. +49 −0 lib/shippinglogic/ups/request.rb
  27. +58 −0 lib/shippinglogic/ups/response.rb
  28. +11 −0 lib/shippinglogic/ups/service.rb
  29. +53 −0 lib/shippinglogic/ups/ship_accept.rb
  30. +170 −0 lib/shippinglogic/ups/ship_confirm.rb
  31. +118 −0 lib/shippinglogic/ups/track.rb
  32. +32 −0 lib/shippinglogic/validation.rb
  33. +67 −0 spec/attributes_spec.rb
  34. 0 spec/{fedex_credentials.example.yaml → config/fedex_credentials.example.yml}
  35. +3 −0 spec/config/ups_credentials.example.yml
  36. +43 −0 spec/error_spec.rb
  37. +0 −58 spec/fedex/attributes_spec.rb
  38. +1 −1 spec/fedex/cancel_spec.rb
  39. +1 −1 spec/fedex/error_spec.rb
  40. +3 −3 spec/fedex/rate_spec.rb
  41. +1 −1 spec/fedex/request_spec.rb
  42. 0 spec/{ → fedex}/responses/blank.xml
  43. 0 spec/{ → fedex}/responses/cancel_not_found.xml
  44. 0 spec/{ → fedex}/responses/failed_authentication.xml
  45. 0 spec/{ → fedex}/responses/malformed.xml
  46. 0 spec/{ → fedex}/responses/rate_defaults.xml
  47. 0 spec/{ → fedex}/responses/rate_insurance.xml
  48. 0 spec/{ → fedex}/responses/rate_no_services.xml
  49. 0 spec/{ → fedex}/responses/rate_non_custom_packaging.xml
  50. 0 spec/{ → fedex}/responses/ship_defaults.xml
  51. 0 spec/{ → fedex}/responses/signature_defaults.xml
  52. 0 spec/{ → fedex}/responses/track_defaults.xml
  53. 0 spec/{ → fedex}/responses/unexpected.xml
  54. +1 −1 spec/fedex/service_spec.rb
  55. +1 −1 spec/fedex/ship_spec.rb
  56. +1 −1 spec/fedex/signature_spec.rb
  57. +83 −0 spec/fedex/spec_helper.rb
  58. +1 −1 spec/fedex/track_spec.rb
  59. +0 −10 spec/fedex/validation_spec.rb
  60. +6 −1 spec/fedex_spec.rb
  61. +42 −0 spec/proxy_spec.rb
  62. +23 −0 spec/service_spec.rb
  63. +4 −87 spec/spec_helper.rb
  64. 0 spec/ups/responses/blank.xml
  65. +2 −0 spec/ups/responses/track_defaults.xml
  66. +43 −0 spec/ups/spec_helper.rb
  67. +16 −0 spec/ups_spec.rb
  68. +48 −0 spec/validation_spec.rb
View
2 .gitignore
@@ -3,4 +3,4 @@
coverage
rdoc
pkg
-spec/fedex_credentials.yml
+spec/*_credentials.yml
View
3 Rakefile
@@ -11,8 +11,9 @@ begin
gem.homepage = "http://github.com/binarylogic/shippinglogic"
gem.authors = ["Ben Johnson of Binary Logic"]
gem.add_development_dependency "rspec"
- gem.add_dependency "activesupport", ">= 2.2.0"
+ gem.add_development_dependency "fakeweb"
gem.add_dependency "httparty", ">= 0.4.4"
+ gem.add_dependency "builder", ">= 2.1.2"
end
Jeweler::GemcutterTasks.new
rescue LoadError
View
1 init.rb
@@ -0,0 +1 @@
+require File.join(File.dirname(__FILE__), "lib", "shippinglogic")
View
6 lib/shippinglogic.rb
@@ -1,3 +1,3 @@
-require "httparty"
-require "activesupport"
-require "shippinglogic/fedex"
+require "shippinglogic/service"
+require "shippinglogic/fedex"
+require "shippinglogic/ups"
View
121 lib/shippinglogic/attributes.rb
@@ -0,0 +1,121 @@
+module Shippinglogic
+ # Adds in all of the reading / writing for the various serivce options.
+ module Attributes
+ def self.included(klass)
+ klass.class_eval do
+ alias_method(:real_class, :class) unless method_defined? :real_class
+
+ extend ClassMethods
+ include InstanceMethods
+ end
+ 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
+ attributes[name] = options
+
+ define_method(name) { read_attribute(name) }
+ 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(*args)
+ attributes = args.last.is_a?(Hash) ? args.last : {}
+ @attributes = {}
+ self.attributes = attributes
+ end
+
+ # Returns a hash of the various attribute values
+ def attributes
+ attributes = {}
+ attribute_names.each do |name|
+ attributes[name] = send(name)
+ end
+ 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|
+ next if !attribute_names.include?(key.to_sym)
+ send("#{key}=", value)
+ end
+ end
+
+ private
+ def attribute_names
+ real_class.attribute_names
+ end
+
+ def attribute_options(name)
+ real_class.attribute_options(name)
+ end
+
+ def attribute_type(name)
+ attribute_options(name)[:type]
+ end
+
+ def attribute_default(name)
+ default = attribute_options(name)[:default]
+ case default
+ when Proc
+ default.call(self)
+ else
+ default
+ end
+ end
+
+ def write_attribute(name, value)
+ @attributes[name.to_sym] = value
+ end
+
+ def read_attribute(name)
+ name = name.to_sym
+ value = @attributes[name].nil? ? attribute_default(name) : @attributes[name]
+ type = attribute_type(name)
+ return nil if value.nil? && type != :array
+
+ case type
+ when :array
+ value.is_a?(Array) ? value : [value].compact
+ when :integer
+ value.to_i
+ when :float
+ value.to_f
+ when :decimal
+ BigDecimal.new(value.to_s)
+ when :boolean
+ ["true", "1"].include?(value.to_s)
+ when :string, :text
+ value.to_s
+ when :datetime
+ Time.parse(value.to_s)
+ else
+ value
+ end
+ end
+ end
+ end
+end
View
13 lib/shippinglogic/error.rb
@@ -0,0 +1,13 @@
+module Shippinglogic
+ class Error < StandardError
+ attr_accessor :errors
+
+ def add_error(error, code = nil)
+ errors << {:message => error, :code => code}
+ end
+
+ def errors
+ @errors ||= []
+ end
+ end
+end
View
13 lib/shippinglogic/fedex.rb
@@ -1,6 +1,4 @@
require "shippinglogic/fedex/enumerations"
-require "shippinglogic/fedex/error"
-require "shippinglogic/fedex/proxy"
require "shippinglogic/fedex/service"
require "shippinglogic/fedex/cancel"
require "shippinglogic/fedex/rate"
@@ -24,14 +22,14 @@ class FedEx
# * <tt>:production_url</tt> - the production URL for FedEx's webservices. (default: https://gateway.fedex.com:443/xml)
def self.options
@options ||= {
- :test => defined?(Rails) && !Rails.env.production?,
+ :test => !!(defined?(Rails) && !Rails.env.production?),
:production_url => "https://gateway.fedex.com:443/xml",
:test_url => "https://gatewaybeta.fedex.com:443/xml"
}
end
-
+
attr_accessor :key, :password, :account, :meter, :options
-
+
# Before you can use the FedEx web services you need to provide 4 credentials:
#
# 1. Your fedex web service key
@@ -58,6 +56,11 @@ def initialize(key, password, account, meter, options = {})
self.options = self.class.options.merge(options)
end
+ # A convenience method for accessing the endpoint URL for the FedEx API.
+ def url
+ options[:test] ? options[:test_url] : options[:production_url]
+ end
+
def cancel(attributes = {})
@cancel ||= Cancel.new(self, attributes)
end
View
119 lib/shippinglogic/fedex/attributes.rb
@@ -1,119 +0,0 @@
-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
- extend ClassMethods
- include InstanceMethods
- end
- 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
- attributes[name] = options
-
- define_method(name) { read_attribute(name) }
- 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|
- attributes[name] = send(name)
- end
- 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|
- next if !attribute_names.include?(key.to_sym)
- send("#{key}=", value)
- end
- end
-
- private
- def attribute_names
- real_class.attribute_names
- end
-
- def attribute_options(name)
- self.real_class.attribute_options(name)
- end
-
- def attribute_type(name)
- attribute_options(name)[:type]
- end
-
- def attribute_default(name)
- default = attribute_options(name)[:default]
- case default
- when Proc
- default.call(self)
- else
- default
- end
- end
-
- def write_attribute(name, value)
- reset_target
- @attributes[name.to_sym] = value
- end
-
- def read_attribute(name)
- name = name.to_sym
- value = @attributes[name].nil? ? attribute_default(name) : @attributes[name]
- type = attribute_type(name)
- return nil if value.nil? && type != :array
-
- case type
- when :array
- value.is_a?(Array) ? value : [value].compact
- when :integer
- value.to_i
- when :float
- value.to_f
- when :decimal
- BigDecimal.new(value.to_s)
- when :boolean
- ["true", "1"].include?(value.to_s)
- when :string, :text
- value.to_s
- else
- value
- end
- end
- end
- end
- end
-end
View
14 lib/shippinglogic/fedex/error.rb
@@ -16,8 +16,8 @@ class FedEx
# # e.response
# # to get the raw response from fedex
# end
- class Error < StandardError
- attr_accessor :errors, :request, :response
+ class Error < Shippinglogic::Error
+ attr_accessor :request, :response
def initialize(request, response)
self.request = request
@@ -44,16 +44,6 @@ def initialize(request, response)
"was in an unexpected format. You might try glancing at the raw response by using the 'response' method on this error object."
)
end
-
- super(errors.collect { |error| error[:message].strip }.to_sentence)
- end
-
- def add_error(error, code = nil)
- errors << {:message => error, :code => code}
- end
-
- def errors
- @errors ||= []
end
end
end
View
21 lib/shippinglogic/fedex/proxy.rb
@@ -1,21 +0,0 @@
-module Shippinglogic
- class FedEx
- class Proxy
- alias_method :real_class, :class
- instance_methods.each { |m| undef_method m unless m =~ /(^__|^real_class$|^send$|^object_id$)/ }
-
- attr_accessor :target
-
- def initialize(target)
- self.target = target
- end
-
- protected
- # We undefined a lot of methods at the beginning of this class. The only methods present in this
- # class are ones that we need, everything else is delegated to our target object.
- def method_missing(name, *args, &block)
- target.send(name, *args, &block)
- end
- end
- end
-end
View
8 lib/shippinglogic/fedex/rate.rb
@@ -199,16 +199,16 @@ def parse_response(response)
delivered_by = details[:delivery_timestamp] && Time.parse(details[:delivery_timestamp])
speed = case details[:service_type]
when /overnight/i
- 1.day
+ 86400 # 1.day
when /2_day/i
- 2.days
+ 172800 # 2.days
else
- 3.days
+ 259200 # 3.days
end
if meets_deadline?(delivered_by)
service = Service.new
- service.name = details[:service_type].titleize
+ service.name = details[:service_type].gsub("_", " ").gsub(/\b(\w)(\w*)/){ $1 + $2.downcase }
service.type = details[:service_type]
service.saturday = details[:applied_options] == "SATURDAY_DELIVERY"
service.delivered_by = delivered_by
View
4 lib/shippinglogic/fedex/request.rb
@@ -1,11 +1,13 @@
+require "builder"
+
module Shippinglogic
class FedEx
# Methods relating to building and sending a request to FedEx's web services.
module Request
private
# Convenience method for sending requests to FedEx
def request(body)
- real_class.post(base.options[:test] ? base.options[:test_url] : base.options[:production_url], :body => body)
+ real_class.post(base.url, :body => body)
end
# Convenience method to create a builder object so that our builder options are consistent across
View
4 lib/shippinglogic/fedex/response.rb
@@ -1,3 +1,5 @@
+require "shippinglogic/fedex/error"
+
module Shippinglogic
class FedEx
# Methods relating to receiving a response from FedEx and cleaning it up.
@@ -63,7 +65,7 @@ def sanitize_response_keys(response)
# I also did not want to use the underscore method provided by ActiveSupport because I am trying
# to avoid using that as a dependency.
def sanitize_response_key(key)
- key.to_s.gsub(/^(v[0-9]|ns):/, "").underscore.to_sym
+ key.to_s.sub(/^(v[0-9]|ns):/, "").gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').gsub(/([a-z\d])([A-Z])/,'\1_\2').downcase.to_sym
end
end
end
View
29 lib/shippinglogic/fedex/service.rb
@@ -1,38 +1,11 @@
-require "shippinglogic/fedex/attributes"
require "shippinglogic/fedex/request"
require "shippinglogic/fedex/response"
-require "shippinglogic/fedex/validation"
module Shippinglogic
class FedEx
- class Service < Proxy
- include Attributes
- include HTTParty
+ class Service < Shippinglogic::Service
include Request
include Response
- include Validation
-
- attr_accessor :base
-
- # Accepts the base service object as a single parameter so that we can access
- # authentication credentials and options.
- def initialize(base, attributes = {})
- self.base = base
- super
- end
-
- private
- # Allows the cached response to be reset, specifically when an attribute changes
- def reset_target
- @target = nil
- end
-
- # For each service you need to overwrite this method. This is where you make the call to fedex
- # and do your magic. See the child classes for examples on how to define this method. It is very
- # important that you cache the result into a variable to avoid uneccessary requests.
- def target
- raise ImplementationError.new("You need to implement a target method that the proxy class can delegate method calls to")
- end
end
end
end
View
6 lib/shippinglogic/fedex/ship.rb
@@ -1,3 +1,5 @@
+require "base64"
+
module Shippinglogic
class FedEx
# An interface to the shipe services provided by FedEx. Allows you to create shipments and get various information on the shipment
@@ -241,8 +243,8 @@ def parse_response(response)
shipment.currency = rate[:currency]
shipment.delivery_date = Date.parse(details[:routing_detail][:delivery_date])
shipment.tracking_number = package_details[:tracking_id][:tracking_number]
- shipment.label = Base64.decode64(package_details[:label][:parts][:image])
- shipment.barcode = Base64.decode64(package_details[:barcodes][:common2_d_barcode])
+ shipment.label = package_details[:label][:parts][:image] && Base64.decode64(package_details[:label][:parts][:image])
+ shipment.barcode = package_details[:barcodes][:common2_d_barcode] && Base64.decode64(package_details[:barcodes][:common2_d_barcode])
shipment
end
end
View
4 lib/shippinglogic/fedex/signature.rb
@@ -1,3 +1,5 @@
+require "base64"
+
module Shippinglogic
class FedEx
# An interface to the signature proof of delivery services provided by FedEx. Allows you to get an image
@@ -58,7 +60,7 @@ def build_request
# in the response.
def parse_response(response)
signature = Signature.new
- signature.image = Base64.decode64(response[:letter])
+ signature.image = response[:letter] && Base64.decode64(response[:letter])
signature
end
end
View
32 lib/shippinglogic/fedex/validation.rb
@@ -1,32 +0,0 @@
-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
- # 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
- true
- rescue Error => e
- errors.clear
- self.errors << e.message
- false
- end
- end
- end
- end
-end
View
19 lib/shippinglogic/proxy.rb
@@ -0,0 +1,19 @@
+module Shippinglogic
+ class Proxy
+ alias_method :real_class, :class
+ instance_methods.each { |m| undef_method m unless m =~ /^(__|real_class$|send$|object_id$)/ }
+
+ attr_accessor :target
+
+ def initialize(target)
+ self.target = target
+ end
+
+ protected
+ # We undefined a lot of methods at the beginning of this class. The only methods present in this
+ # class are ones that we need, everything else is delegated to our target object.
+ def method_missing(name, *args, &block)
+ target.send(name, *args, &block)
+ end
+ end
+end
View
42 lib/shippinglogic/service.rb
@@ -0,0 +1,42 @@
+require "httparty"
+require "shippinglogic/proxy"
+require "shippinglogic/attributes"
+require "shippinglogic/validation"
+
+module Shippinglogic
+ class Service < Proxy
+ include Attributes
+ include HTTParty
+ include Validation
+
+ attr_accessor :base
+
+ # Accepts the base service object as a single parameter so that we can access
+ # authentication credentials and options.
+ def initialize(base, attributes = {})
+ self.base = base
+ super
+ end
+
+ private
+ # Allows the cached response to be reset, specifically when an attribute changes
+ def reset_target
+ @target = nil
+ end
+
+ # Resets the target before deferring to the +write_attribute+ method as defined by the
+ # +Attributes+ module. This keeps +Attributes+ dissociated from any proxy or service specific
+ # code, making testing simpler.
+ def write_attribute(*args)
+ reset_target
+ super
+ end
+
+ # For each service you need to overwrite this method. This is where you make the call to the API
+ # and do your magic. See the child classes for examples on how to define this method. It is very
+ # important that you cache the result into a variable to avoid uneccessary requests.
+ def target
+ raise Error.new("You need to implement a target method that the proxy class can delegate method calls to")
+ end
+ end
+end
View
83 lib/shippinglogic/ups.rb
@@ -0,0 +1,83 @@
+require "shippinglogic/ups/service"
+require "shippinglogic/ups/cancel"
+require "shippinglogic/ups/rate"
+require "shippinglogic/ups/track"
+
+module Shippinglogic
+ class UPS
+ # A hash representing default the options. If you are using this in a Rails app the best place
+ # to modify or change these options is either in an initializer or your specific environment file. Keep
+ # in mind that these options can be modified on the instance level when creating an object. See #initialize
+ # for more details.
+ #
+ # === Options
+ #
+ # * <tt>:test</tt> - this basically tells us which url to use. If set to true we will use the UPS test URL, if false we
+ # will use the production URL. If you are using this in a rails app, unless you are in your production environment, this
+ # will default to true automatically.
+ # * <tt>:test_url</tt> - the test URL for UPS's webservices. (default: https://wwwcie.ups.com:443/ups.app/xml)
+ # * <tt>:production_url</tt> - the production URL for UPS's webservices. (default: https://www.ups.com:443/ups.app/xml)
+ def self.options
+ @options ||= {
+ :test => !!(defined?(Rails) && !Rails.env.production?),
+ :production_url => "https://www.ups.com:443/ups.app/xml",
+ :test_url => "https://wwwcie.ups.com:443/ups.app/xml"
+ }
+ end
+
+ attr_accessor :key, :password, :account, :number, :options
+
+ # Before you can use the UPS web services you need to provide 4 credentials:
+ #
+ # 1. Your UPS access key
+ # 2. Your UPS password
+ # 3. Your UPS user ID
+ # 4. Your 6-character UPS account number
+ #
+ #TODO Explain how to acquire those 4 credentials.
+ #
+ # The last parameter allows you to modify the class options on an instance level. It accepts the
+ # same options that the class level method #options accepts. If you don't want to change any of
+ # them, don't supply this parameter.
+ def initialize(key, password, account, number, options = {})
+ self.key = key
+ self.password = password
+ self.account = account
+ self.number = number
+ self.options = self.class.options.merge(options)
+ end
+
+ # A convenience method for accessing the endpoint URL for the UPS API.
+ def url
+ options[:test] ? options[:test_url] : options[:production_url]
+ end
+
+ def cancel(attributes = {})
+ @cancel ||= Cancel.new(self, attributes)
+ end
+
+ def rate(attributes = {})
+ @rate ||= Rate.new(self, attributes)
+ end
+
+ def ship_confirm(attributes = {})
+ @ship_confirm ||= ShipConfirm.new(self, attributes)
+ end
+
+ def ship_accept(attributes = {})
+ @ship_accept ||= ShipAccept.new(self, attributes)
+ end
+
+ def ship(attributes = {})
+ @ship ||= ship_accept(:digest => ship_confirm(attributes).digest)
+ end
+
+ def track(attributes = {})
+ @track ||= Track.new(self, attributes)
+ end
+
+ def label(attributes = {})
+ @label ||= Label.new(self, attributes)
+ end
+ end
+end
View
52 lib/shippinglogic/ups/cancel.rb
@@ -0,0 +1,52 @@
+module Shippinglogic
+ class UPS
+ # An interface to the shipment canceling service provided by UPS. Allows you to cancel a shipment
+ #
+ # == Accessor methods / options
+ #
+ # * <tt>tracking_number</tt> - the tracking number
+ #
+ # === Simple Example
+ #
+ # ups = Shippinglogic::UPS.new(key, password, account)
+ # cancel = ups.cancel(:tracking_number => "my number")
+ # cancel.perform
+ # # => true
+ class Cancel < Service
+ def self.path
+ "/Void"
+ end
+
+ attribute :tracking_number, :string
+
+ # Our services are set up as a proxy. We need to access the underlying object, to trigger the request
+ # to UPS. So calling this method is a way to do that since there really is no underlying object
+ def perform
+ target && true
+ end
+
+ private
+ # The parent class Service requires that we define this method. This is our kicker. This method is only
+ # called when we need to deal with information from FedEx. Notice the caching into the @target variable.
+ def target
+ @target ||= request(build_request)
+ end
+
+ # Just building some XML to send off to FedEx. FedEx require this particualr format.
+ def build_request
+ b = builder
+ build_authentication(b)
+ b.instruct!
+
+ b.VoidShipmentRequest do
+ b.Request do
+ b.RequestAction "1"
+ end
+
+ #TODO Determine whether tracking numbers are valid shipment identification numbers.
+ b.ShipmentIdentificationNumber tracking_number
+ end
+ end
+ end
+ end
+end
View
56 lib/shippinglogic/ups/enumerations.rb
@@ -0,0 +1,56 @@
+module Shippinglogic
+ class UPS
+ # This module contains the various enumerations that UPS uses for its various options. When describing
+ # service options sometimes the docs will specify that the option must be an item in one of these arrays.
+ # You can also use these to build drop down options.
+ #
+ # Lastly, if you want to make these user friendly use a string inflector (humanize or titlize).
+ module Enumerations
+ # packaging options
+ PACKAGING_TYPES = {
+ "00" => "UNKNOWN",
+ "01" => "UPS Letter",
+ "02" => "Package",
+ "03" => "Tube",
+ "04" => "Pak",
+ "21" => "Express Box",
+ "24" => "25KG Box",
+ "25" => "10KG Box",
+ "30" => "Pallet",
+ "2a" => "Small Express Box",
+ "2b" => "Medium Express Box",
+ "2c" => "Large Express Box"
+ }
+
+ # delivery options
+ DROPOFF_TYPES = {
+ "01" => "Daily Pickup",
+ "03" => "Customer Counter",
+ "06" => "One Time Pickup",
+ "07" => "On Call Air",
+ "11" => "Suggested Retail Rates",
+ "19" => "Letter Center",
+ "20" => "Air Service Center"
+ }
+ SERVICE_TYPES = {
+ "01" => "Next Day Air",
+ "02" => "2nd Day Air",
+ "03" => "Ground",
+ "07" => "Worldwide Express",
+ "08" => "Worldwide Expedited",
+ "11" => "Standard",
+ "12" => "3 Day Select",
+ "13" => "Next Day Air Saver",
+ "14" => "Next Day Air Early AM",
+ "54" => "Worldwide Express Plus",
+ "59" => "2nd Day Air AM",
+ "65" => "Saver",
+ "82" => "UPS Today Standard",
+ "83" => "UPS Today Dedicated Courier",
+ "84" => "UPS Today Intercity",
+ "85" => "UPS Today Express",
+ "86" => "UPS Today Express Saver"
+ }
+ end
+ end
+end
View
45 lib/shippinglogic/ups/error.rb
@@ -0,0 +1,45 @@
+module Shippinglogic
+ class UPS
+ # If UPS responds with an error, we try our best to pull the pertinent information out of that
+ # response and raise it with this object. Any time UPS says there is a problem an object of this
+ # class will be raised.
+ #
+ # === Tip
+ #
+ # If you want to see the raw request / respose catch the error object and call the request / response method. Ex:
+ #
+ # begin
+ # # my UPS code
+ # rescue Shippinglogic::UPS::Error => e
+ # # do whatever you want here, just do:
+ # # e.request
+ # # e.response
+ # # to get the raw response from UPS
+ # end
+ class Error < Shippinglogic::Error
+ attr_accessor :request, :response
+
+ def initialize(request, response)
+ self.request = request
+ self.response = response
+
+ if response.blank?
+ add_error("The response from UPS was blank.")
+ elsif !response.is_a?(Hash)
+ add_error("The response from UPS was malformed and was not in a valid XML format.")
+ elsif errors = response.fetch(:response, {})[:error]
+ errors = errors.is_a?(Array) ? errors : [errors]
+ errors.delete_if { |error| Response::SUCCESSFUL_SEVERITIES.include?(error[:error_severity]) }
+ errors.each { |error| add_error(error[:error_description], error[:error_code]) }
+ else
+ add_error(
+ "There was a problem with your UPS request, and we couldn't locate a specific error message. This means your response " +
+ "was in an unexpected format. You might try glancing at the raw response by using the 'response' method on this error object."
+ )
+ end
+
+ super(errors.collect { |error| error[:message] }.join(", "))
+ end
+ end
+ end
+end
View
50 lib/shippinglogic/ups/label.rb
@@ -0,0 +1,50 @@
+require "base64"
+
+module Shippinglogic
+ class UPS
+ class Label < Service
+ def self.path
+ "/LabelRecovery"
+ end
+
+ attribute :tracking_number, :string
+
+ private
+ def target
+ @target ||= parse_response(request(build_request))
+ end
+
+ # Just building some XML to send off to USP using our various options
+ def build_request
+ b = builder
+ build_authentication(b)
+ b.instruct!
+
+ b.LabelRecoveryRequest do
+ b.Request do
+ b.RequestAction "LabelRecovery"
+ end
+
+ b.LabelSpecification do
+ b.LabelImageFormat do
+ b.Code "GIF"
+ end
+ end
+
+ b.Translate do
+ b.LanguageCode "eng"
+ b.DialectCode "US"
+ b.Code "01"
+ end
+
+ b.TrackingNumber tracking_number
+ end
+ end
+
+ def parse_response(response)
+ return unless details = response.fetch(:label_results, {})[:label_image]
+ Base64.decode64(details[:graphic_image])
+ end
+ end
+ end
+end
View
228 lib/shippinglogic/ups/rate.rb
@@ -0,0 +1,228 @@
+module Shippinglogic
+ class UPS
+ # An interface to the rate services provided by UPS. Allows you to get an array of rates from UPS for a shipment,
+ # or a single rate for a specific service.
+ #
+ # == Options
+ # === Shipper options
+ #
+ # * <tt>shipper_name</tt> - name of the shipper.
+ # * <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. UPS expects abbreviations, but Shippinglogic will convert full names to abbreviations for you.
+ #
+ # === Recipient options
+ #
+ # * <tt>recipient_name</tt> - name of the recipient.
+ # * <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. UPS expects abbreviations, but Shippinglogic will convert full names to abbreviations for you.
+ # * <tt>recipient_residential</tt> - a boolean value representing if the address is redential or not (default: false)
+ #
+ # === Packaging options
+ #
+ # One thing to note is that UPS does support multiple package shipments. The problem is that all of the packages must be identical.
+ # UPS specifically notes in their documentation that mutiple package specifications are not allowed. So your only option for a
+ # multi package shipment is to increase the package_count option and keep the dimensions and weight the same for all packages. Then again,
+ # the documentation for the UPS web services is terrible, so I could be wrong. Any tests I tried resulted in an error though.
+ #
+ # * <tt>packaging_type</tt> - one of PACKAGE_TYPES. (default: YOUR_PACKAGING)
+ # * <tt>package_count</tt> - the number of packages in your shipment. (default: 1)
+ # * <tt>package_weight</tt> - a single packages weight.
+ # * <tt>package_weight_units</tt> - either LB or KG. (default: LB)
+ # * <tt>package_length</tt> - a single packages length, only required if using YOUR_PACKAGING for packaging_type.
+ # * <tt>package_width</tt> - a single packages width, only required if using YOUR_PACKAGING for packaging_type.
+ # * <tt>package_height</tt> - a single packages height, only required if using YOUR_PACKAGING for packaging_type.
+ # * <tt>package_dimension_units</tt> - either IN or CM. (default: IN)
+ #
+ # === Monetary options
+ #
+ # * <tt>currency_type</tt> - the type of currency. (default: nil, because UPS will default to your account preferences)
+ # * <tt>insured_value</tt> - the value you want to insure, if any. (default: nil)
+ # * <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: your account number)
+ #
+ # === Delivery options
+ #
+ # * <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>delivery_deadline</tt> - whether or not to include estimated transit times. (default: true)
+ # * <tt>dropoff_type</tt> - one of DROP_OFF_TYPES. (default: REGULAR_PICKUP)
+ #
+ # === Misc options
+ #
+ # * <tt>documents_only</tt> - whether the package consists of only documents (default: false)
+ #
+ # == Simple Example
+ #
+ # Here is a very simple example. Mix and match the options above to get more accurate rates:
+ #
+ # ups = Shippinglogic::UPS.new(key, password, account)
+ # rates = ups.rate(
+ # :shipper_postal_code => "10007",
+ # :shipper_country => "US",
+ # :recipient_postal_code => "75201",
+ # :recipient_country_code => "US",
+ # :package_weight => 24,
+ # :package_length => 12,
+ # :package_width => 12,
+ # :package_height => 12
+ # )
+ #
+ # rates.first
+ # #<Shippinglogic::UPS::Rate::Service:0x10354d108 @currency="USD", @speed=nil,
+ # @rate=#<BigDecimal:10353ac10,'0.1885E2',18(18)>, @type="Ground", @name="Ground">
+ #
+ # # to show accessor methods
+ # rates.first.name
+ # # => "Ground"
+ class Rate < Service
+ def self.path
+ "/Rate"
+ end
+
+ # Each rate result is an object of this class
+ class Service; attr_accessor :name, :type, :speed, :rate, :currency; end
+
+ # shipper options
+ attribute :shipper_name, :string
+ attribute :shipper_streets, :string
+ attribute :shipper_city, :string
+ attribute :shipper_state, :string
+ attribute :shipper_postal_code, :string
+ attribute :shipper_country, :string, :modifier => :country_code
+
+ # recipient options
+ attribute :recipient_name, :string
+ attribute :recipient_streets, :string
+ attribute :recipient_city, :string
+ attribute :recipient_state, :string
+ attribute :recipient_postal_code, :string
+ attribute :recipient_country, :string, :modifier => :country_code
+ attribute :recipient_residential, :boolean, :default => false
+
+ # packaging options
+ attribute :packaging_type, :string, :default => "00"
+ attribute :package_count, :integer, :default => 1
+ attribute :package_weight, :float
+ attribute :package_weight_units, :string, :default => "LBS"
+ attribute :package_length, :float
+ attribute :package_width, :float
+ attribute :package_height, :float
+ attribute :package_dimension_units, :string, :default => "IN"
+
+ # monetary options
+ attribute :currency_type, :string
+ attribute :insured_value, :decimal
+ attribute :payor_account_number, :string, :default => lambda { |shipment| shipment.base.account }
+
+ # delivery options
+ attribute :service_type, :string
+ attribute :dropoff_type, :string, :default => "01"
+ attribute :saturday, :boolean, :default => false
+
+ # misc options
+ attribute :documents_only, :boolean, :default => false
+
+ private
+ def target
+ @target ||= parse_response(request(build_request))
+ end
+
+ def build_request
+ b = builder
+ build_authentication(b)
+ b.instruct!
+
+ b.RatingServiceSelectionRequest do
+ b.Request do
+ b.RequestAction "Rate"
+ b.RequestOption service_type ? "Rate" : "Shop"
+ end
+
+ b.PickupType do
+ b.Code dropoff_type
+ end
+
+ b.Shipment do
+ b.Shipper do
+ b.Name shipper_name
+ b.ShipperNumber payor_account_number
+ build_address(b, :shipper)
+ end
+
+ b.ShipTo do
+ b.CompanyName recipient_name
+ build_address(b, :recipient)
+ end
+
+ if service_type
+ b.Service do
+ b.Code service_type
+ end
+ end
+
+ b.DocumentsOnly if documents_only
+
+ package_count.times do |i|
+ b.Package do
+ b.PackagingType do
+ b.Code packaging_type
+ end
+
+ b.Dimensions do
+ b.UnitOfMeasurement do
+ b.Code package_dimension_units
+ end
+
+ b.Length "%.2f" % package_length
+ b.Width "%.2f" % package_width
+ b.Height "%.2f" % package_height
+ end
+
+ b.PackageWeight do
+ b.UnitOfMeasurement do
+ b.Code package_weight_units
+ end
+
+ b.Weight "%.1f" % package_weight
+ end
+
+ b.PackageServiceOptions do
+ if insured_value
+ b.InsuredValue do
+ b.MonetaryValue insured_value
+ b.CurrencyCode currency_type
+ end
+ end
+ end
+ end
+ end
+
+ b.ShipmentServiceOptions do
+ b.SaturdayDelivery if saturday
+ end
+ end
+ end
+ end
+
+ def parse_response(response)
+ return [] if !response[:rated_shipment]
+
+ response[:rated_shipment].collect do |details|
+ service = Service.new
+ service.name = Enumerations::SERVICE_TYPES[details[:service][:code]]
+ service.type = service.name
+ service.speed = (days = details[:guaranteed_days_to_delivery]) && (days.to_i * 86400)
+ service.rate = BigDecimal.new(details[:total_charges][:monetary_value])
+ service.currency = details[:total_charges][:currency_code]
+ service
+ end
+ end
+ end
+ end
+end
View
49 lib/shippinglogic/ups/request.rb
@@ -0,0 +1,49 @@
+require "builder"
+
+module Shippinglogic
+ class UPS
+ # Methods relating to building and sending a request to UPS's web services.
+ module Request
+ private
+ # Convenience method for sending requests to UPS
+ def request(body)
+ real_class.post(base.url + real_class.path, :body => body)
+ end
+
+ # Convenience method to create a builder object so that our builder options are consistent across
+ # the various services.
+ #
+ # Ex: if I want to change the indent level to 3 it should change for all requests built.
+ def builder
+ b = Builder::XmlMarkup.new(:indent => 2)
+ b.instruct!
+ b
+ end
+
+ # A convenience method for building the authentication block in your XML request
+ def build_authentication(b)
+ b.AccessRequest(:"xml:lang" => "en-US") do
+ b.AccessLicenseNumber base.key
+ b.UserId base.account
+ b.Password base.password
+ end
+ end
+
+ # A convenience method for building the address block in your XML request
+ def build_address(b, type)
+ address_lines = send("#{type}_streets").to_s.split(/(?:\s*\n\s*)+/m, 3)
+
+ b.Address do
+ b.AddressLine1 address_lines[0] if address_lines[0]
+ b.AddressLine2 address_lines[1] if address_lines[1]
+ b.AddressLine3 address_lines[2] if address_lines[2]
+ b.City send("#{type}_city") if send("#{type}_city")
+ b.StateProvinceCode send("#{type}_state") if send("#{type}_state")
+ b.PostalCode send("#{type}_postal_code") if send("#{type}_postal_code")
+ b.CountryCode send("#{type}_country") if send("#{type}_country")
+ b.ResidentialAddressIndicator attribute_names.include?("#{type}_residential") && send("#{type}_residential")
+ end
+ end
+ end
+ end
+end
View
58 lib/shippinglogic/ups/response.rb
@@ -0,0 +1,58 @@
+require "shippinglogic/ups/error"
+
+module Shippinglogic
+ class UPS
+ # Methods relating to receiving a response from UPS and cleaning it up.
+ module Response
+ SUCCESSFUL_SEVERITIES = ["Warning"]
+
+ private
+ # Overwriting the request method to clean the response and handle errors.
+ def request(body)
+ response = clean_response(super)
+
+ if success?(response)
+ response
+ else
+ raise Error.new(body, response)
+ end
+ end
+
+ # Was the response a success?
+ def success?(response)
+ response.is_a?(Hash) && response[:response][:response_status_code] == "1"
+ end
+
+ # Cleans the response and returns it in a more 'user friendly' format that is easier
+ # to work with.
+ def clean_response(response)
+ cut_to_the_chase(sanitize_response_keys(response))
+ end
+
+ # UPS likes nested XML tags, because they send quite a bit of them back in responses.
+ # This method just 'cuts to the chase' and get to the heart of the response.
+ def cut_to_the_chase(response)
+ response.values.first
+ end
+
+ # Recursively sanitizes the response object by clenaing up any hash keys.
+ def sanitize_response_keys(response)
+ if response.is_a?(Hash)
+ response.inject({}) do |r, (key, value)|
+ r[sanitize_response_key(key)] = sanitize_response_keys(value)
+ r
+ end
+ elsif response.is_a?(Array)
+ response.collect { |r| sanitize_response_keys(r) }
+ else
+ response
+ end
+ end
+
+ # Underscores and symbolizes incoming UPS response keys.
+ def sanitize_response_key(key)
+ key.gsub(/([a-z])([A-Z])/, '\1_\2').downcase.to_sym
+ end
+ end
+ end
+end
View
11 lib/shippinglogic/ups/service.rb
@@ -0,0 +1,11 @@
+require "shippinglogic/ups/request"
+require "shippinglogic/ups/response"
+
+module Shippinglogic
+ class UPS
+ class Service < Shippinglogic::Service
+ include Request
+ include Response
+ end
+ end
+end
View
53 lib/shippinglogic/ups/ship_accept.rb
@@ -0,0 +1,53 @@
+require "base64"
+
+module Shippinglogic
+ class UPS
+ class ShipAccept < Service
+ def self.path
+ "/ShipAccept"
+ end
+
+ class Details
+ class Shipment; attr_accessor :tracking_number, :label; end
+
+ attr_accessor :rate, :currency, :shipments
+
+ def initialize(response)
+ details = response[:shipment_results]
+
+ charges = details[:shipment_charges][:total_charges]
+ self.rate = BigDecimal.new(charges[:monetary_value])
+ self.currency = charges[:currency_code]
+
+ self.shipments = [*details[:package_results]].collect do |package|
+ shipment = Shipment.new
+ shipment.tracking_number = package[:tracking_number]
+ shipment.label = Base64.decode64(package[:label_image][:graphic_image])
+ shipment
+ end
+ end
+ end
+
+ attribute :digest, :string
+
+ private
+ def target
+ @target ||= Details.new(request(build_request))
+ end
+
+ def build_request
+ b = builder
+ build_authentication(b)
+ b.instruct!
+
+ b.ShipmentAcceptRequest do
+ b.Request do
+ b.RequestAction "ShipAccept"
+ end
+
+ b.ShipmentDigest digest
+ end
+ end
+ end
+ end
+end
View
170 lib/shippinglogic/ups/ship_confirm.rb
@@ -0,0 +1,170 @@
+module Shippinglogic
+ class UPS
+ class ShipConfirm < Service
+ def self.path
+ "/ShipConfirm"
+ end
+
+ class Details
+ attr_accessor :digest, :tracking_number, :rate, :currency
+
+ def initialize(response)
+ self.digest = response[:shipment_digest]
+ self.tracking_number = response[:shipment_identification_number]
+
+ charges = response[:shipment_charges][:total_charges]
+ self.rate = BigDecimal.new(charges[:monetary_value])
+ self.currency = charges[:currency_code]
+ end
+ end
+
+ # shipper options
+ attribute :shipper_name, :string
+ attribute :shipper_phone_number, :string
+ attribute :shipper_email, :string
+ attribute :shipper_streets, :string
+ attribute :shipper_city, :string
+ attribute :shipper_state, :string
+ attribute :shipper_postal_code, :string
+ attribute :shipper_country, :string
+
+ # recipient options
+ attribute :recipient_name, :string
+ attribute :recipient_phone_number, :string
+ attribute :recipient_email, :string
+ attribute :recipient_streets, :string
+ attribute :recipient_city, :string
+ attribute :recipient_state, :string
+ attribute :recipient_postal_code, :string
+ attribute :recipient_country, :string
+ attribute :recipient_residential, :boolean, :default => false
+
+ # label options
+ attribute :label_format, :string, :default => "GIF"
+ attribute :label_file_type, :string, :default => "GIF"
+
+ # packaging options
+ attribute :packaging_type, :string, :default => "00"
+ attribute :package_count, :integer, :default => 1
+ attribute :package_weight, :float
+ attribute :package_weight_units, :string, :default => "LBS"
+ attribute :package_length, :integer
+ attribute :package_width, :integer
+ attribute :package_height, :integer
+ attribute :package_dimension_units, :string, :default => "IN"
+
+ # monetary options
+ attribute :currency_type, :string
+ attribute :insured_value, :decimal
+ attribute :payor_account_number, :string, :default => lambda { |shipment| shipment.base.number }
+
+ # delivery options
+ attribute :service_type, :string
+ #FIXME Setting the signature option to true raises and error. I believe this has something
+ # to do with UPS account-specific settings and signature service availability.
+ attribute :signature, :boolean, :default => false
+ attribute :saturday, :boolean, :default => false
+
+ private
+ def target
+ @target ||= Details.new(request(build_request))
+ end
+
+ def build_request
+ b = builder
+ build_authentication(b)
+ b.instruct!
+
+ b.ShipmentConfirmRequest do
+ b.Request do
+ b.RequestAction "ShipConfirm"
+ b.RequestOption "validate"
+ end
+
+ b.Shipment do
+ b.Shipper do
+ b.Name shipper_name
+ b.ShipperNumber payor_account_number
+ b.PhoneNumber shipper_phone_number
+ b.EMailAddress shipper_email
+ build_address(b, :shipper)
+ end
+
+ b.ShipTo do
+ b.CompanyName recipient_name
+ b.PhoneNumber recipient_phone_number
+ b.EMailAddress recipient_email
+ build_address(b, :recipient)
+ end
+
+ b.PaymentInformation do
+ b.Prepaid do
+ b.BillShipper do
+ b.AccountNumber payor_account_number
+ end
+ end
+ end
+
+ b.Service do
+ b.Code service_type
+ end
+
+ b.ShipmentServiceOptions do
+ b.SaturdayDelivery if saturday
+ if signature
+ b.DeliveryConfirmation do
+ b.DCISType "1"
+ end
+ end
+ end
+
+ package_count.times do |i|
+ b.Package do
+ b.PackagingType do
+ b.Code packaging_type
+ end
+
+ b.Dimensions do
+ b.UnitOfMeasurement do
+ b.Code package_dimension_units
+ end
+
+ b.Length "%.2f" % package_length
+ b.Width "%.2f" % package_width
+ b.Height "%.2f" % package_height
+ end
+
+ b.PackageWeight do
+ b.UnitOfMeasurement do
+ b.Code package_weight_units
+ end
+
+ b.Weight "%.1f" % package_weight
+ end
+
+ b.PackageServiceOptions do
+ if insured_value
+ b.InsuredValue do
+ b.MonetaryValue insured_value
+ b.CurrencyCode currency_type
+ end
+ end
+ end
+ end
+ end
+ end
+
+ b.LabelSpecification do
+ b.LabelPrintMethod do
+ b.Code label_file_type
+ end
+
+ b.LabelImageFormat do
+ b.Code label_format
+ end
+ end
+ end
+ end
+ end
+ end
+end
View
118 lib/shippinglogic/ups/track.rb
@@ -0,0 +1,118 @@
+module Shippinglogic
+ class UPS
+ # An interface to the track services provided by UPS. 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:
+ #
+ # ups = Shippinglogic::UPS.new(key, password, account)
+ # tracking_details = ups.track(:tracking_number => "my number")
+ #
+ # tracking_details.status
+ # # => "Delivered"
+ #
+ # tracking_details.signature_name
+ # # => "KKING"
+ #
+ # tracking_details.events.first
+ # # => #<Shippinglogic::UPS::Track::Event @postal_code="95817", @name="Delivered", @state="CA",
+ # # @city="Sacramento", @type="Delivered", @country="US", @occured_at=Mon Dec 08 10:43:37 -0500 2008>
+ #
+ # tracking_details.events.first.name
+ # # => "Delivered"
+ #
+ # === Note
+ #
+ # UPS 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
+ def self.path
+ "/Track"
+ end
+
+ class Details
+ # Each tracking result is an object of this class
+ class Event; attr_accessor :name, :type, :occured_at, :city, :state, :postal_code, :country; end
+
+ attr_accessor :origin_city, :origin_state, :origin_country,
+ :destination_city, :destination_state, :destination_country,
+ :signature_name, :service_type, :status, :delivery_at,
+ :events
+
+ def initialize(response)
+ details = response[:shipment]
+
+ if origin = details.fetch(:shipper, {})[:address]
+ self.origin_city = origin[:city]
+ self.origin_state = origin[:state_province_code]
+ self.origin_country = origin[:country_code]
+ end
+
+ if destination = details.fetch(:ship_to, {})[:address]
+ self.destination_city = destination[:city]
+ self.destination_state = destination[:state_province_code]
+ self.destination_country = destination[:country_code]
+ end
+
+ package = details[:package]
+ events = package[:activity].is_a?(Array) ? package[:activity] : [package[:activitiy]].compact
+ last_event = events.first
+ delivery = events.detect{|e| e[:status][:status_type][:code] == "D" }
+
+ self.signature_name = last_event && last_event[:signed_for_by_name]
+ self.service_type = details[:service][:description]
+ self.status = last_event && last_event[:status][:status_type][:description]
+ self.delivery_at = delivery && Time.parse(delivery[:date] + delivery[:time])
+
+ self.events = events.collect do |details|
+ event = Event.new
+ status = details[:status][:status_type]
+ event.name = status[:description]
+ event.type = status[:code]
+ #FIXME The proper spelling is "occurred", not "occured."
+ event.occured_at = Time.parse(details[:date] + details[:time])
+ location = details[:activity_location][:address]
+ event.city = location[:city]
+ event.state = location[:state_province_code]
+ event.postal_code = location[:postal_code]
+ event.country = location[:country_code]
+ event
+ end
+ end
+ end
+
+ attribute :tracking_number, :string
+
+ private
+ # The parent class Service requires that we define this method. This is our kicker. This method is only
+ # called when we need to deal with information from UPS. Notice the caching into the @target variable.
+ def target
+ @target ||= Details.new(request(build_request))
+ end
+
+ # Just building some XML to send off to UPS. UPS requires this particualar format.
+ def build_request
+ b = builder
+ build_authentication(b)
+ b.instruct!
+
+ b.TrackRequest do
+ b.Request do
+ b.RequestAction "Track"
+ b.RequestOption "activity"
+ end
+
+ b.TrackingNumber tracking_number
+ end
+ end
+ end
+ end
+end
View
32 lib/shippinglogic/validation.rb
@@ -0,0 +1,32 @@
+require "shippinglogic/error"
+
+module Shippinglogic
+ # 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
+ # 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 API
+ # services, so what this does is make a call to the API and rescue any errors, then it puts those
+ # errors into the 'errors' array.
+ def valid?
+ begin
+ target
+ true
+ rescue Error => e
+ errors.clear
+ self.errors << e.message
+ false
+ end
+ end
+ end
+end
View
67 spec/attributes_spec.rb
@@ -0,0 +1,67 @@
+require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
+
+describe "Service Attributes" do
+ before(:all) do
+ class ::ServiceWithAttributes
+ include Shippinglogic::Attributes
+
+ attribute :tracking_number, :string
+ attribute :packaging_type, :string, :default => "YOUR_PACKAGING"
+ attribute :special_services_requested, :array
+ attribute :ship_time, :datetime, :default => lambda { |shipment| Time.now }
+ end
+ end
+
+ after(:all) do
+ Object.send(:remove_const, :ServiceWithAttributes)
+ end
+
+ it "should allow setting attributes upon initialization" do
+ service = ServiceWithAttributes.new(:tracking_number => "TEST")
+ service.tracking_number.should == "TEST"
+ end
+
+ it "should allow setting attributes individually" do
+ service = ServiceWithAttributes.new
+ service.tracking_number = "TEST"
+ service.tracking_number.should == "TEST"
+ end
+
+ it "should allow setting attributes with a hash" do
+ service = ServiceWithAttributes.new
+ service.attributes = {:tracking_number => "TEST"}
+ service.tracking_number.should == "TEST"
+ end
+
+ it "should allow reading attributes" do
+ service = ServiceWithAttributes.new
+ service.attributes = {:tracking_number => "TEST"}
+ service.attributes.should be_a(Hash)
+ service.attributes.should have_key(:tracking_number)
+ service.attributes[:tracking_number].should == "TEST"
+ end
+
+ it "should implement defaults" do
+ service = ServiceWithAttributes.new
+ service.packaging_type.should == "YOUR_PACKAGING"
+ end
+
+ it "should use blank array as defaults for arrays" do
+ service = ServiceWithAttributes.new
+ service.special_services_requested.should == []
+ end
+
+ it "should call procs during run time if a default is a proc" do
+ service = ServiceWithAttributes.new
+ service.ship_time.to_s.should == Time.now.to_s
+ end
+
+ it "should parse string representations of times" do
+ service = ServiceWithAttributes.new
+ service.ship_time = "19551105000000"
+ service.ship_time.should be_a(Time)
+ service.ship_time.strftime("%B") == "November"
+ service.ship_time.day.should == 5
+ service.ship_time.year.should == 1955
+ end
+end
View
0 spec/fedex_credentials.example.yaml → spec/config/fedex_credentials.example.yml
File renamed without changes.
View
3 spec/config/ups_credentials.example.yml
@@ -0,0 +1,3 @@
+key: your key
+password: your password
+account: your account username
View
43 spec/error_spec.rb
@@ -0,0 +1,43 @@
+require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
+
+describe "Shippinglogic Errors" do
+ before(:each) do
+ @error = Shippinglogic::Error.new
+ end
+
+ it "should inherit from StandardError" do
+ @error.should be_a(StandardError)
+ end
+
+ it "should contain an array of errors" do
+ @error.errors.should == []
+ end
+
+ it "should be able to add error hashes" do
+ @error.errors.size.should == 0
+ @error.add_error("MESSAGE")
+ @error.errors.size.should == 1
+ @error.errors.last.should be_a(Hash)
+ end
+
+ it "should append (not prepend) error hashes" do
+ @error.add_error("FIRST")
+ @error.errors.last[:message].should == "FIRST"
+ @error.add_error("LAST")
+ @error.errors.last[:message].should == "LAST"
+ end
+
+ it "should have error hashes containing a required message" do
+ @error.add_error("MESSAGE")
+ @error.errors.last.should have_key(:message)
+ @error.errors.last[:message].should == "MESSAGE"
+ end
+
+ it "should have error hashes containing an optional code" do
+ @error.add_error("WITHOUT")
+ @error.errors.last.should have_key(:code)
+ @error.errors.last[:code].should be_nil
+ @error.add_error("WITH", "CODE")
+ @error.errors.last[:code].should == "CODE"
+ end
+end
View
58 spec/fedex/attributes_spec.rb
@@ -1,58 +0,0 @@
-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.rate
- rates.shipper_residential.should == false
- end
-
- it "should use blank array as defaults for arrays" do
- rates = new_fedex.rate
- rates.special_services_requested.should == []
- end
-
- it "should call procs during run time if a default is a proc" do
- rates = new_fedex.rate
- rates.ship_time.to_s.should == Time.now.to_s
- end
-
- it "should reset the cached response if an attribute changes" do
- use_response(:rate_defaults)
-
- fedex = new_fedex
- rates = fedex.rate
- rates.attributes = fedex_shipper
- rates.attributes = fedex_recipient
- rates.attributes = fedex_package
- rates.size.should == 6
-
- use_response(:rate_non_custom_packaging)
-
- rates.packaging_type = "FEDEX_ENVELOPE"
- rates.package_weight = 0.1
- rates.size.should == 5
- end
-end
View
2 spec/fedex/cancel_spec.rb
@@ -1,4 +1,4 @@
-require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
+require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
describe "FedEx Track" do
it "should cancel the shipment" do
View
2 spec/fedex/error_spec.rb
@@ -1,4 +1,4 @@
-require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
+require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
describe "FedEx Error" do
it "should handle blank response errors" do
View
6 spec/fedex/rate_spec.rb
@@ -1,4 +1,4 @@
-require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
+require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
describe "FedEx Rate" do
it "should rate the shipment" do
@@ -17,7 +17,7 @@
rate.type.should == "FIRST_OVERNIGHT"
rate.saturday.should == false
rate.delivered_by.should == Time.parse("Fri Aug 07 08:00:00 -0400 2009")
- rate.speed.should == 1.day
+ rate.speed.should == 86400 # 1.day
rate.rate.should == 70.01
rate.currency.should == "USD"
end
@@ -40,7 +40,7 @@
rate.type.should == "FIRST_OVERNIGHT"
rate.saturday.should == false
rate.delivered_by.should == Time.parse("Mon Aug 10 08:00:00 -0400 2009")
- rate.speed.should == 1.day
+ rate.speed.should == 86400 # 1.day
rate.rate.should == 50.43
rate.currency.should == "USD"
end
View
2 spec/fedex/request_spec.rb
@@ -1,4 +1,4 @@
-require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
+require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
describe "FedEx Attributes" do
it "should convert full country names to country codes" do
View
0 spec/responses/blank.xml → spec/fedex/responses/blank.xml
File renamed without changes.
View
0 spec/responses/cancel_not_found.xml → spec/fedex/responses/cancel_not_found.xml
File renamed without changes.
View
0 spec/responses/failed_authentication.xml → ...fedex/responses/failed_authentication.xml
File renamed without changes.
View
0 spec/responses/malformed.xml → spec/fedex/responses/malformed.xml
File renamed without changes.
View
0 spec/responses/rate_defaults.xml → spec/fedex/responses/rate_defaults.xml
File renamed without changes.
View
0 spec/responses/rate_insurance.xml → spec/fedex/responses/rate_insurance.xml
File renamed without changes.
View
0 spec/responses/rate_no_services.xml → spec/fedex/responses/rate_no_services.xml
File renamed without changes.
View
0 spec/responses/rate_non_custom_packaging.xml → ...x/responses/rate_non_custom_packaging.xml
File renamed without changes.
View
0 spec/responses/ship_defaults.xml → spec/fedex/responses/ship_defaults.xml
File renamed without changes.
View
0 spec/responses/signature_defaults.xml → spec/fedex/responses/signature_defaults.xml
File renamed without changes.
View
0 spec/responses/track_defaults.xml → spec/fedex/responses/track_defaults.xml
File renamed without changes.
View
0 spec/responses/unexpected.xml → spec/fedex/responses/unexpected.xml
File renamed without changes.
View
2 spec/fedex/service_spec.rb
@@ -1,4 +1,4 @@
-require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
+require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
describe "FedEx Service" do
it "should not hit fedex until needed" do
View
2 spec/fedex/ship_spec.rb
@@ -1,4 +1,4 @@
-require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
+require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
describe "FedEx Ship" do
before(:each) do
View
2 spec/fedex/signature_spec.rb
@@ -1,4 +1,4 @@
-require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
+require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
describe "FedEx Signature" do
it "should return an image of the signature" do
View
83 spec/fedex/spec_helper.rb
@@ -0,0 +1,83 @@
+require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
+
+Shippinglogic::FedEx.options[:test] = true
+
+Spec::Runner.configure do |config|
+ config.before(:each) do
+ FakeWeb.clean_registry
+
+ if File.exists?("#{SPEC_ROOT}/fedex/responses/_new.xml")
+ raise "You have a new response in your response folder, you need to rename this before we can continue testing."
+ end
+ end
+
+ def new_fedex
+ Shippinglogic::FedEx.new(*fedex_credentials.values_at("key", "password", "account", "meter"))
+ end
+
+ def fedex_credentials
+ return @fedex_credentials if defined?(@fedex_credentials)
+
+ fedex_credentials_path = "#{SPEC_ROOT}/config/fedex_credentials.yml"
+
+ unless File.exists?(fedex_credentials_path)
+ raise "You need to add your own FedEx test credentials in spec/config/fedex_credentials.yml. See spec/config/fedex_credentials.example.yml for an example."
+ end
+
+ @fedex_credentials = YAML.load(File.read(fedex_credentials_path))
+ end
+
+ def fedex_tracking_number
+ "077973360403984"
+ end
+
+ def fedex_shipper
+ {
+ :shipper_name => "Name",
+ :shipper_title => "Title",
+ :shipper_company_name => "Company",
+ :shipper_phone_number => "2222222222",
+ :shipper_email => "a@a.com",
+ :shipper_streets => "260 Broadway",
+ :shipper_city => "New York",
+ :shipper_state => "NY",
+ :shipper_postal_code => "10007",
+ :shipper_country => "US"
+ }
+ end
+
+ def fedex_recipient
+ {
+ :recipient_name => "Name",
+ :recipient_title => "Title",
+ :recipient_department => "Department",
+ :recipient_company_name => "Dallas City Hall",
+ :recipient_phone_number => "2222222222",
+ :recipient_email => "a@a.com",
+ :recipient_streets => "1500 Marilla Street",
+ :recipient_city => "Dallas",
+ :recipient_state => "TX",
+ :recipient_postal_code => "75201",
+ :recipient_country => "US"
+ }
+ end
+
+ def fedex_package
+ {
+ :package_weight => 2,
+ :package_length => 2,
+ :package_width => 2,
+ :package_height => 2
+ }
+ end
+
+ def use_response(key, options = {})
+ path = "#{SPEC_ROOT}/fedex/responses/#{key}.xml"
+ if File.exists?(path)
+ options[:content_type] ||= "text/xml"
+ options[:body] ||= File.read(path)
+ url = Shippinglogic::FedEx.options[:test_url]
+ FakeWeb.register_uri(:post, url, options)
+ end
+ end
+end
View
2 spec/fedex/track_spec.rb
@@ -1,4 +1,4 @@
-require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
+require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
describe "FedEx Track" do
it "should track the package" do
View
10 spec/fedex/validation_spec.rb
@@ -1,10 +0,0 @@
-require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
-
-describe "FedEx Validation" do
- it "should not be valid" do
- use_response(:blank)
- rates = new_fedex.rate
- rates.valid?.should == false
- rates.errors.should == ["The response from FedEx was blank."]
- end
-end
View
7 spec/fedex_spec.rb
@@ -2,10 +2,15 @@
describe "FedEx" do
it "should return the default options" do
+ spec_helper_options = Shippinglogic::FedEx.options
+ Shippinglogic::FedEx.instance_variable_set("@options", nil)
+
Shippinglogic::FedEx.options.should == {
- :test => true,
+ :test => false,
:production_url => "https://gateway.fedex.com:443/xml",
:test_url => "https://gatewaybeta.fedex.com:443/xml"
}
+
+ Shippinglogic::FedEx.instance_variable_set("@options", spec_helper_options)
end
end
View
42 spec/proxy_spec.rb