module Shippinglogic
class 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.
#
# == Options
# === Shipper options
#
# * shipper_streets - street part of the address, separate multiple streets with a new line, dont include blank lines.
# * shipper_city - city part of the address.
# * shipper_state_ - state part of the address, use state abreviations.
# * shipper_postal_code - postal code part of the address. Ex: zip for the US.
# * shipper_country - country code part of the address. FedEx expects abbreviations, but Shippinglogic will convert full names to abbreviations for you.
# * shipper_residential - a boolean value representing if the address is redential or not (default: false)
#
# === Recipient options
#
# * recipient_streets - street part of the address, separate multiple streets with a new line, dont include blank lines.
# * recipient_city - city part of the address.
# * recipient_state - state part of the address, use state abreviations.
# * recipient_postal_code - postal code part of the address. Ex: zip for the US.
# * recipient_country - country code part of the address. FedEx expects abbreviations, but Shippinglogic will convert full names to abbreviations for you.
# * recipient_residential - a boolean value representing if the address is redential or not (default: false)
#
# === Packaging options
#
# One thing to note is that FedEx does support multiple package shipments. The problem is that all of the packages must be identical.
# FedEx 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 FedEx web services is terrible, so I could be wrong. Any tests I tried resulted in an error though.
#
# * packaging_type - one of PACKAGE_TYPES. (default: YOUR_PACKAGING)
# * package_count - the number of packages in your shipment. (default: 1)
# * package_weight - a single packages weight.
# * package_weight_units - either LB or KG. (default: LB)
# * package_length - a single packages length, only required if using YOUR_PACKAGING for packaging_type.
# * package_width - a single packages width, only required if using YOUR_PACKAGING for packaging_type.
# * package_height - a single packages height, only required if using YOUR_PACKAGING for packaging_type.
# * package_dimension_units - either IN or CM. (default: IN)
#
# === Monetary options
#
# * currency_type - the type of currency. (default: nil, because FedEx will default to your account preferences)
# * insured_value - the value you want to insure, if any. (default: nil)
# * payment_type - one of PAYMENT_TYPES. (default: SENDER)
# * payor_account_number - if the account paying for this ship is different than the account you specified then
# you can specify that here. (default: your account number)
# * payor_country - the country code for the account number. (default: US)
#
# === Delivery options
#
# * ship_time - a Time object representing when you want to ship the package. (default: Time.now)
# * service_type - one of SERVICE_TYPES, this is optional, leave this blank if you want a list of all
# available services. (default: nil)
# * delivery_deadline - whether or not to include estimated transit times. (default: true)
# * dropoff_type - one of DROP_OFF_TYPES. (default: REGULAR_PICKUP)
# * special_services_requested - any exceptions or special services FedEx needs to be aware of, this should be
# one or more of SPECIAL_SERVICES. (default: nil)
#
# === Misc options
#
# * rate_request_types - one or more of RATE_REQUEST_TYPES. (default: ACCOUNT)
# * include_transit_times - whether or not to include estimated transit times. (default: true)
#
# == 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)
# rates = fedex.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
# #,
# @deadline=Fri Aug 07 08:00:00 -0400 2009, @type="FIRST_OVERNIGHT", @saturday=false>
#
# # to show accessor methods
# rates.first.name
# # => "First Overnight"
class Rate < Service
# Each rate result is an object of this class
class Service; attr_accessor :name, :type, :saturday, :delivered_by, :rate, :currency; end
VERSION = {:major => 6, :intermediate => 0, :minor => 0}
# shipper options
attribute :shipper_streets, :string
attribute :shipper_city, :string
attribute :shipper_state, :string
attribute :shipper_postal_code, :string
attribute :shipper_country, :string, :modifier => :country_code
attribute :shipper_residential, :boolean, :default => false
# recipient options
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 => "YOUR_PACKAGING"
attribute :package_count, :integer, :default => 1
attribute :package_weight, :float
attribute :package_weight_units, :string, :default => "LB"
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, :big_decimal
attribute :payment_type, :string, :default => "SENDER"
attribute :payor_account_number, :string, :default => lambda { |shipment| shipment.base.account }
attribute :payor_country, :string
# delivery options
attribute :ship_time, :datetime, :default => lambda { |rate| Time.now }
attribute :service_type, :string
attribute :delivery_deadline, :datetime
attribute :dropoff_type, :string, :default => "REGULAR_PICKUP"
attribute :special_services_requested, :array
# misc options
attribute :rate_request_types, :array, :default => ["ACCOUNT"]
attribute :include_transit_times, :boolean, :default => true
private
def target
@target ||= parse_response(request(build_request))
end
def build_request
b = builder
xml = b.RateRequest(:xmlns => "http://fedex.com/ws/rate/v#{VERSION[:major]}") do
build_authentication(b)
build_version(b, "crs", VERSION[:major], VERSION[:intermediate], VERSION[:minor])
b.ReturnTransitAndCommit include_transit_times
b.SpecialServicesRequested special_services_requested.join(",") if special_services_requested.any?
b.RequestedShipment do
b.ShipTimestamp ship_time.xmlschema if ship_time
b.ServiceType service_type if service_type
b.DropoffType dropoff_type if dropoff_type
b.PackagingType packaging_type if packaging_type
b.TotalInsuredValue insured_value if insured_value
b.Shipper { build_address(b, :shipper) }
b.Recipient { build_address(b, :recipient) }
b.ShippingChargesPayment do
b.PaymentType payment_type if payment_type
b.Payor do
b.AccountNumber payor_account_number if payor_account_number
b.CountryCode payor_country if payor_country
end
end
b.RateRequestTypes rate_request_types.join(",") if rate_request_types
build_package(b)
end
end
end
def parse_response(response)
return [] if !response[:rate_reply_details]
response[:rate_reply_details].collect do |details|
shipment_detail = details[:rated_shipment_details].is_a?(Array) ? details[:rated_shipment_details].first : details[:rated_shipment_details]
cost = shipment_detail[:shipment_rate_detail][:total_net_charge]
delivered_by = details[:delivery_timestamp] && Time.parse(details[:delivery_timestamp])
if meets_deadline?(delivered_by)
service = Service.new
service.name = details[:service_type].titleize
service.type = details[:service_type]
service.saturday = details[:applied_options] == "SATURDAY_DELIVERY"
service.delivered_by = delivered_by
service.rate = BigDecimal.new(cost[:amount])
service.currency = cost[:currency]
service
end
end.compact
end
def meets_deadline?(delivered_by)
return true if !delivery_deadline
delivered_by && delivered_by <= delivery_deadline
end
end
end
end