binarylogic-shippinglogic 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,199 @@
1
+ module Shippinglogic
2
+ class FedEx
3
+ # An interface to the rate services provided by FedEx. Allows you to get an array of rates from fedex for a shipment,
4
+ # or a single rate for a specific service.
5
+ #
6
+ # == Accessor methods / options
7
+ #
8
+ # * <tt>shipper_streets</tt> - street part of the address, separate multiple streets with a new line, dont include blank lines.
9
+ # * <tt>shipper_city</tt> - city part of the address.
10
+ # * <tt>shipper_state_</tt> - state part of the address, use state abreviations.
11
+ # * <tt>shipper_postal_code</tt> - postal code part of the address. Ex: zip for the US.
12
+ # * <tt>shipper_country</tt> - country code part of the address, use abbreviations, ex: 'US'
13
+ # * <tt>shipper_residential</tt> - a boolean value representing if the address is redential or not (default: false)
14
+ # * <tt>recipient_streets</tt> - street part of the address, separate multiple streets with a new line, dont include blank lines.
15
+ # * <tt>recipient_city</tt> - city part of the address.
16
+ # * <tt>recipient_state</tt> - state part of the address, use state abreviations.
17
+ # * <tt>recipient_postal_code</tt> - postal code part of the address. Ex: zip for the US.
18
+ # * <tt>recipient_country</tt> - country code part of the address, use abbreviations, ex: 'US'
19
+ # * <tt>recipient_residential</tt> - a boolean value representing if the address is redential or not (default: false)
20
+ # * <tt>service_type</tt> - one of SERVICE_TYPES, this is optional, leave this blank if you want a list of all
21
+ # available services. (default: nil)
22
+ # * <tt>packaging_type</tt> - one of PACKAGE_TYPES. (default: YOUR_PACKAGING)
23
+ # * <tt>packages</tt> - an array of packages included in the shipment. This should be an array of hashes with the following keys:
24
+ # * <tt>:weight</tt> - the weight
25
+ # * <tt>:weight_units</tt> - either LB or KG. (default: LB)
26
+ # * <tt>:length</tt> - the length.
27
+ # * <tt>:width</tt> - the width.
28
+ # * <tt>:height</tt> - the height.
29
+ # * <tt>:dimension_units</tt> - either IN or CM. (default: IN)
30
+ # * <tt>ship_time</tt> - a Time object representing when you want to ship the package. (default: Time.now)
31
+ # * <tt>dropoff_type</tt> - one of DROP_OFF_TYPES. (default: REGULAR_PICKUP)
32
+ # * <tt>include_transit_times</tt> - whether or not to include estimated transit times. (default: true)
33
+ # * <tt>delivery_deadline</tt> - whether or not to include estimated transit times. (default: true)
34
+ # * <tt>special_services_requested</tt> - any exceptions or special services FedEx needs to be aware of, this should be
35
+ # one or more of SPECIAL_SERVICES. (default: nil)
36
+ # * <tt>currency_type</tt> - the type of currency. (default: nil, because FedEx will default to your account preferences)
37
+ # * <tt>rate_request_types</tt> - one or more of RATE_REQUEST_TYPES. (default: ACCOUNT)
38
+ # * <tt>insured_value</tt> - the value you want to insure, if any. (default: nil)
39
+ # * <tt>payment_type</tt> - one of PAYMENT_TYPES. (default: SENDER)
40
+ # * <tt>payor_account_number</tt> - if the account paying for this ship is different than the account you specified then
41
+ # you can specify that here. (default: nil)
42
+ # * <tt>payor_country</tt> - the country code for the account number. (default: US)
43
+ #
44
+ # === Simple Example
45
+ #
46
+ # Here is a very simple example. Mix and match the options above to get more accurate rates:
47
+ #
48
+ # fedex = Shippinglogic::FedEx.new(key, password, account, meter)
49
+ # fedex.rates(
50
+ # :shipper_postal_code => "10007",
51
+ # :shipper_country => "US",
52
+ # :recipient_postal_code => "75201",
53
+ # :recipient_country_code => "US"
54
+ # :packages => [{:weight => 24, :length => 12, :width => 12, :height => 12}]
55
+ # )
56
+ class Rates < Service
57
+ # Each rate result is an object of this class
58
+ class Rate; attr_accessor :name, :type, :saturday, :deadline, :cost, :currency; end
59
+
60
+ VERSION = {:major => 6, :intermediate => 0, :minor => 0}
61
+ DROP_OFF_TYPES = ["REGULAR_PICKUP", "REQUEST_COURIER", "DROP_BOX", "BUSINESS_SERVICE_CENTER", "STATION"]
62
+ PACKAGE_TYPES = ["FEDEX_ENVELOPE", "FEDEX_PAK", "FEDEX_BOX", "FEDEX_TUBE", "FEDEX_10KG_BOX", "FEDEX_25KG_BOX", "YOUR_PACKAGING"]
63
+ SPECIAL_SERVICES = [
64
+ "APPOINTMENT_DELIVERY", "DANGEROUS_GOODS", "DRY_ICE", "NON_STANDARD_CONTAINER", "PRIORITY_ALERT", "SIGNATURE_OPTION",
65
+ "FEDEX_FREIGHT", "FEDEX_NATIONAL_FREIGHT", "INSIDE_PICKUP", "INSIDE_DELIVERY", "EXHIBITION", "EXTREME_LENGTH", "FLATBED_TRAILER",
66
+ "FREIGHT_GUARANTEE", "LIFTGATE_DELIVERY", "LIFTGATE_PICKUP", "LIMITED_ACCESS_DELIVERY", "LIMITED_ACCESS_PICKUP", "PRE_DELIVERY_NOTIFICATION",
67
+ "PROTECTION_FROM_FREEZING", "REGIONAL_MALL_DELIVERY", "REGIONAL_MALL_PICKUP"
68
+ ]
69
+ RATE_REQUEST_TYPES = ["ACCOUNT", "LIST", "MULTIWEIGHT"]
70
+ PAYMENT_TYPES = ["SENDER", "CASH", "CREDIT_CARD"]
71
+ SERVICE_TYPES = [
72
+ "INTERNATIONAL_FIRST", "FEDEX_3_DAY_FREIGHT", "STANDARD_OVERNIGHT", "PRIORITY_OVERNIGHT", "FEDEX_GROUND", "INTERNATIONAL_PRIORITY",
73
+ "FIRST_OVERNIGHT", "FEDEX_1_DAY_FREIGHT", "FEDEX_2_DAY_FREIGHT", "INTERNATIONAL_GROUND", "INTERNATIONAL_ECONOMY_FREIGHT", "INTERNATIONAL_ECONOMY",
74
+ "FEDEX_1_DAY_FREIGHT_SATURDAY_DELIVERY", "INTERNATIONAL_PRIORITY_FREIGHT", "FEDEX_2_DAY_FREIGHT_SATURDAY_DELIVERY", "FEDEX_2_DAY_SATURDAY_DELIVERY",
75
+ "FEDEX_3_DAY_FREIGHT_SATURDAY_DELIVERY", "FEDEX_2_DAY", "PRIORITY_OVERNIGHT_SATURDAY_DELIVERY", "INTERNATIONAL_PRIORITY_SATURDAY_DELIVERY",
76
+ "FEDEX_EXPRESS_SAVER", "GROUND_HOME_DELIVERY"
77
+ ]
78
+
79
+ attribute :shipper_streets, :string
80
+ attribute :shipper_city, :string
81
+ attribute :shipper_state, :string
82
+ attribute :shipper_postal_code, :string
83
+ attribute :shipper_country, :string
84
+ attribute :shipper_residential, :boolean, :default => false
85
+
86
+ attribute :recipient_streets, :string
87
+ attribute :recipient_city, :string
88
+ attribute :recipient_state, :string
89
+ attribute :recipient_postal_code, :string
90
+ attribute :recipient_country, :string
91
+ attribute :recipient_residential, :boolean, :default => false
92
+
93
+ attribute :service_type, :string
94
+ attribute :packaging_type, :string, :default => "YOUR_PACKAGING"
95
+ attribute :packages, :array
96
+ attribute :ship_time, :datetime, :default => lambda { Time.now }
97
+ attribute :dropoff_type, :boolean, :default => "REGULAR_PICKUP"
98
+ attribute :include_transit_times, :boolean, :default => true
99
+ attribute :delivery_deadline, :datetime
100
+ attribute :special_services_requested, :array
101
+ attribute :currency_type, :string
102
+ attribute :rate_request_types, :array, :default => ["ACCOUNT"]
103
+ attribute :insured_value, :big_decimal
104
+ attribute :payment_type, :string, :default => "SENDER"
105
+ attribute :payor_account_number, :string
106
+ attribute :payor_country, :string, :default => "US"
107
+
108
+ private
109
+ def target
110
+ @target ||= parse_rate_response(request(build_rate_request))
111
+ end
112
+
113
+ def build_rate_request
114
+ b = builder
115
+ xml = b.RateRequest(:xmlns => "http://fedex.com/ws/rate/v#{VERSION[:major]}") do
116
+ build_authentication(b)
117
+ build_version(b, "crs", VERSION[:major], VERSION[:intermediate], VERSION[:minor])
118
+ b.ReturnTransitAndCommit include_transit_times
119
+ b.SpecialServicesRequested special_services_requested.join(",") if special_services_requested.any?
120
+
121
+ b.RequestedShipment do
122
+ b.ShipTimestamp ship_time.xmlschema
123
+ b.ServiceType service_type if service_type
124
+ b.DropoffType dropoff_type if dropoff_type
125
+ b.PackagingType packaging_type if packaging_type
126
+ b.TotalInsuredValue insured_value if insured_value
127
+ b.Shipper { build_address(b, :shipper) }
128
+ b.Recipient { build_address(b, :recipient) }
129
+ b.ShippingChargesPayment do
130
+ b.PaymentType payment_type
131
+ if payor_account_number && payor_country_code
132
+ b.Payor do
133
+ b.AccountNumber payor_account_number
134
+ b.CountryCode payor_country
135
+ end
136
+ end
137
+ end
138
+ b.RateRequestTypes rate_request_types.join(",")
139
+ b.PackageCount packages.size
140
+ b.PackageDetail "INDIVIDUAL_PACKAGES"
141
+
142
+ packages.each { |package| build_package(b, package) }
143
+ end
144
+ end
145
+ end
146
+
147
+ def build_address(b, type)
148
+ b.Address do
149
+ b.StreetLines send("#{type}_streets") if send("#{type}_streets")
150
+ b.City send("#{type}_city") if send("#{type}_city")
151
+ b.StateOrProvinceCode send("#{type}_state") if send("#{type}_state")
152
+ b.PostalCode send("#{type}_postal_code") if send("#{type}_postal_code")
153
+ b.CountryCode send("#{type}_country") if send("#{type}_country")
154
+ b.Residential send("#{type}_residential")
155
+ end
156
+ end
157
+
158
+ def build_package(b, package)
159
+ package.symbolize_keys! if package.respond_to?(:symbolize_keys!)
160
+ validate_package(package)
161
+
162
+ b.RequestedPackages do
163
+ b.Weight do
164
+ b.Units package[:weight_units] || "LB"
165
+ b.Value package[:weight]
166
+ end
167
+
168
+ b.Dimensions do
169
+ b.Length package[:length]
170
+ b.Width package[:width]
171
+ b.Height package[:height]
172
+ b.Units package[:dimension_units] || "IN"
173
+ end
174
+ end
175
+ end
176
+
177
+ def validate_package(package)
178
+ raise ArgumentError.new("Each package much be in a Hash format") if !package.is_a?(Hash)
179
+ package.assert_valid_keys(:weight, :weight_units, :length, :height, :width, :dimension_units)
180
+ end
181
+
182
+ def parse_rate_response(response)
183
+ response[:rate_reply_details].collect do |details|
184
+ shipment_detail = details[:rated_shipment_details].is_a?(Array) ? details[:rated_shipment_details].first : details[:rated_shipment_details]
185
+ cost = shipment_detail[:shipment_rate_detail][:total_net_charge]
186
+
187
+ rate = Rate.new
188
+ rate.name = details[:service_type].titleize
189
+ rate.type = details[:service_type]
190
+ rate.saturday = details[:applied_options] == "SATURDAY_DELIVERY"
191
+ rate.deadline = details[:delivery_timestamp] && Time.parse(details[:delivery_timestamp])
192
+ rate.cost = BigDecimal.new(cost[:amount])
193
+ rate.currency = cost[:currency]
194
+ rate
195
+ end
196
+ end
197
+ end
198
+ end
199
+ end
@@ -0,0 +1,47 @@
1
+ module Shippinglogic
2
+ class FedEx
3
+ # Methods relating to building and sending a request to FedEx's web services.
4
+ module Request
5
+ private
6
+ # Convenience method for sending requests to FedEx
7
+ def request(body)
8
+ self.class.post(base.options[:test] ? base.options[:test_url] : base.options[:production_url], :body => body)
9
+ end
10
+
11
+ # Convenience method to create a builder object so that our builder options are consistent across
12
+ # the various services.
13
+ #
14
+ # Ex: if I want to change the indent level to 3 it should change for all requests built.
15
+ def builder
16
+ b = Builder::XmlMarkup.new(:indent => 2)
17
+ b.instruct!
18
+ b
19
+ end
20
+
21
+ # A convenience method for building the authentication block in your XML request
22
+ def build_authentication(b)
23
+ b.WebAuthenticationDetail do
24
+ b.UserCredential do
25
+ b.Key base.key
26
+ b.Password base.password
27
+ end
28
+ end
29
+
30
+ b.ClientDetail do
31
+ b.AccountNumber base.account
32
+ b.MeterNumber base.meter
33
+ end
34
+ end
35
+
36
+ # A convenience method for building the version block in your XML request
37
+ def build_version(b, service, major, intermediate, minor)
38
+ b.Version do
39
+ b.ServiceId service
40
+ b.Major major
41
+ b.Intermediate intermediate
42
+ b.Minor minor
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,68 @@
1
+ module Shippinglogic
2
+ class FedEx
3
+ # Methods relating to receiving a response from FedEx and cleaning it up.
4
+ module Response
5
+ private
6
+ # Overwriting the request method to clean the response and handle errors.
7
+ def request(*args)
8
+ response = clean_response(super)
9
+
10
+ if success?(response)
11
+ response
12
+ else
13
+ raise Error.new(response)
14
+ end
15
+ end
16
+
17
+ # Was the response a success?
18
+ def success?(response)
19
+ response.is_a?(Hash) && ["SUCCESS", "NOTE"].include?(response[:highest_severity])
20
+ end
21
+
22
+ # Cleans the response and returns it in a more 'user friendly' format that is easier
23
+ # to work with.
24
+ def clean_response(response)
25
+ cut_to_the_chase(sanitize_response_keys(response))
26
+ end
27
+
28
+ # FedEx likes nested XML tags, because they send quite a bit of them back in responses.
29
+ # This method just 'cuts to the chase' and get to the heart of the response.
30
+ def cut_to_the_chase(response)
31
+ if response.is_a?(Hash) && response.keys.first && response.keys.first.to_s =~ /_reply(_details)?$/
32
+ response.values.first
33
+ else
34
+ response
35
+ end
36
+ end
37
+
38
+ # Recursively sanitizes the response object by clenaing up any hash keys.
39
+ def sanitize_response_keys(response)
40
+ if response.is_a?(Hash)
41
+ response.inject({}) do |r, (key, value)|
42
+ r[sanitize_response_key(key)] = sanitize_response_keys(value)
43
+ r
44
+ end
45
+ elsif response.is_a?(Array)
46
+ response.collect { |r| sanitize_response_keys(r) }
47
+ else
48
+ response
49
+ end
50
+ end
51
+
52
+ # FedEx returns a SOAP response. I just want the plain response without all of the SOAP BS.
53
+ # It basically turns this:
54
+ #
55
+ # {"v3:ServiceInfo" => ...}
56
+ #
57
+ # into:
58
+ #
59
+ # {:service_info => ...}
60
+ #
61
+ # I also did not want to use the underscore method provided by ActiveSupport because I am trying
62
+ # to avoid using that as a dependency.
63
+ def sanitize_response_key(key)
64
+ key.to_s.gsub(/^(v[0-9]|ns):/, "").underscore.to_sym
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,40 @@
1
+ require "shippinglogic/fedex/attributes"
2
+ require "shippinglogic/fedex/request"
3
+ require "shippinglogic/fedex/response"
4
+ require "shippinglogic/fedex/validation"
5
+
6
+ module Shippinglogic
7
+ class FedEx
8
+ class Service
9
+ instance_methods.each { |m| undef_method m unless m =~ /(^__|^nil\?$|^class$|^send$|proxy_|^object_id$)/ }
10
+
11
+ include Attributes
12
+ include HTTParty
13
+ include Request
14
+ include Response
15
+ include Validation
16
+
17
+ attr_accessor :base
18
+
19
+ # Accepts the base service object as a single parameter so that we can access
20
+ # authentication credentials and options.
21
+ def initialize(base, attributes = {})
22
+ self.base = base
23
+ super
24
+ end
25
+
26
+ private
27
+ # We undefined a lot of methods at the beginning of this class. The only methods present in this
28
+ # class are ones that we need, everything else is delegated to our target object.
29
+ def method_missing(name, *args, &block)
30
+ target.send(name, *args, &block)
31
+ end
32
+
33
+ # For each service you need to overwrite this method. This is where you make the call to fedex
34
+ # and do your magic.
35
+ def target
36
+ raise ImplementationError.new("You need to implement a target method that the proxy class can delegate method calls to")
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,66 @@
1
+ module Shippinglogic
2
+ class FedEx
3
+ # An interface to the track services provided by FedEx. Allows you to get an array of events for a specific
4
+ # tracking number.
5
+ #
6
+ # == Accessor methods / options
7
+ #
8
+ # * <tt>tracking_number</tt> - the tracking number
9
+ #
10
+ # === Simple Example
11
+ #
12
+ # Here is a very simple example:
13
+ #
14
+ # fedex = Shippinglogic::FedEx.new(key, password, account, meter)
15
+ # fedex.track(:tracking_number => "my number")
16
+ #
17
+ # === Note
18
+ # FedEx does support locating packages through means other than a tracking number.
19
+ # These are not supported and probably won't be until someone needs them. It should
20
+ # be fairly simple to add, but I could not think of a reason why anyone would want to track
21
+ # a package with anything other than a tracking number.
22
+ class Track < Service
23
+ # Each tracking result is an object of this class
24
+ class Event; attr_accessor :name, :type, :occured_at, :city, :state, :postal_code, :country, :residential; end
25
+
26
+ VERSION = {:major => 3, :intermediate => 0, :minor => 0}
27
+
28
+ attribute :tracking_number, :string
29
+
30
+ private
31
+ def target
32
+ @target ||= parse_track_response(request(build_track_request))
33
+ end
34
+
35
+ def build_track_request
36
+ b = builder
37
+ xml = b.TrackRequest(:xmlns => "http://fedex.com/ws/track/v#{VERSION[:major]}") do
38
+ build_authentication(b)
39
+ build_version(b, "trck", VERSION[:major], VERSION[:intermediate], VERSION[:minor])
40
+
41
+ b.PackageIdentifier do
42
+ b.Value tracking_number
43
+ b.Type "TRACKING_NUMBER_OR_DOORTAG"
44
+ end
45
+
46
+ b.IncludeDetailedScans true
47
+ end
48
+ end
49
+
50
+ def parse_track_response(response)
51
+ response[:track_details][:events].collect do |details|
52
+ event = Event.new
53
+ event.name = details[:event_description]
54
+ event.type = details[:event_type]
55
+ event.occured_at = Time.parse(details[:timestamp])
56
+ event.city = details[:address][:city]
57
+ event.state = details[:address][:state_or_province_code]
58
+ event.postal_code = details[:address][:postal_code]
59
+ event.country = details[:address][:country_code]
60
+ event.residential = details[:address][:residential] == "true"
61
+ event
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,32 @@
1
+ module Shippinglogic
2
+ class FedEx
3
+ # This module is more for application integration, so you can do something like:
4
+ #
5
+ # tracking = fedex.tracking
6
+ # if tracking.valid?
7
+ # # render a successful response
8
+ # else
9
+ # # do something with the errors: fedex.errors
10
+ # end
11
+ module Validation
12
+ # Just an array of errors that were encounted if valid? returns false.
13
+ def errors
14
+ @errors ||= []
15
+ end
16
+
17
+ # Allows you to determine if the request is valid or not. All validation is delegated to the FedEx
18
+ # services, so what this does is make a call to FedEx and rescue any errors, then it puts those
19
+ # error into the 'errors' array.
20
+ def valid?
21
+ begin
22
+ target
23
+ true
24
+ rescue Error => e
25
+ errors.clear
26
+ self.errors << e.message
27
+ false
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end