binarylogic-shippinglogic 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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