shippinglogic 1.2.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (74) hide show
  1. data/.document +5 -0
  2. data/CHANGELOG.rdoc +55 -0
  3. data/LICENSE +20 -0
  4. data/README.rdoc +175 -0
  5. data/Rakefile +37 -0
  6. data/VERSION.yml +5 -0
  7. data/init.rb +1 -0
  8. data/lib/shippinglogic.rb +3 -0
  9. data/lib/shippinglogic/attributes.rb +121 -0
  10. data/lib/shippinglogic/error.rb +22 -0
  11. data/lib/shippinglogic/fedex.rb +84 -0
  12. data/lib/shippinglogic/fedex/cancel.rb +47 -0
  13. data/lib/shippinglogic/fedex/enumerations.rb +348 -0
  14. data/lib/shippinglogic/fedex/error.rb +47 -0
  15. data/lib/shippinglogic/fedex/rate.rb +229 -0
  16. data/lib/shippinglogic/fedex/request.rb +134 -0
  17. data/lib/shippinglogic/fedex/response.rb +72 -0
  18. data/lib/shippinglogic/fedex/service.rb +11 -0
  19. data/lib/shippinglogic/fedex/ship.rb +238 -0
  20. data/lib/shippinglogic/fedex/signature.rb +68 -0
  21. data/lib/shippinglogic/fedex/track.rb +124 -0
  22. data/lib/shippinglogic/proxy.rb +23 -0
  23. data/lib/shippinglogic/service.rb +42 -0
  24. data/lib/shippinglogic/ups.rb +83 -0
  25. data/lib/shippinglogic/ups/cancel.rb +52 -0
  26. data/lib/shippinglogic/ups/enumerations.rb +56 -0
  27. data/lib/shippinglogic/ups/error.rb +42 -0
  28. data/lib/shippinglogic/ups/label.rb +50 -0
  29. data/lib/shippinglogic/ups/rate.rb +228 -0
  30. data/lib/shippinglogic/ups/request.rb +49 -0
  31. data/lib/shippinglogic/ups/response.rb +58 -0
  32. data/lib/shippinglogic/ups/service.rb +11 -0
  33. data/lib/shippinglogic/ups/ship_accept.rb +53 -0
  34. data/lib/shippinglogic/ups/ship_confirm.rb +170 -0
  35. data/lib/shippinglogic/ups/track.rb +118 -0
  36. data/lib/shippinglogic/validation.rb +32 -0
  37. data/shippinglogic.gemspec +120 -0
  38. data/spec/attributes_spec.rb +67 -0
  39. data/spec/config/fedex_credentials.example.yml +4 -0
  40. data/spec/config/ups_credentials.example.yml +3 -0
  41. data/spec/error_spec.rb +43 -0
  42. data/spec/fedex/cancel_spec.rb +10 -0
  43. data/spec/fedex/error_spec.rb +26 -0
  44. data/spec/fedex/rate_spec.rb +87 -0
  45. data/spec/fedex/request_spec.rb +15 -0
  46. data/spec/fedex/responses/blank.xml +0 -0
  47. data/spec/fedex/responses/cancel_not_found.xml +7 -0
  48. data/spec/fedex/responses/failed_authentication.xml +7 -0
  49. data/spec/fedex/responses/malformed.xml +8 -0
  50. data/spec/fedex/responses/rate_defaults.xml +7 -0
  51. data/spec/fedex/responses/rate_insurance.xml +9 -0
  52. data/spec/fedex/responses/rate_no_services.xml +7 -0
  53. data/spec/fedex/responses/rate_non_custom_packaging.xml +7 -0
  54. data/spec/fedex/responses/ship_defaults.xml +7 -0
  55. data/spec/fedex/responses/ship_with_no_signature.xml +7 -0
  56. data/spec/fedex/responses/signature_defaults.xml +7 -0
  57. data/spec/fedex/responses/track_defaults.xml +7 -0
  58. data/spec/fedex/responses/unexpected.xml +1 -0
  59. data/spec/fedex/service_spec.rb +19 -0
  60. data/spec/fedex/ship_spec.rb +37 -0
  61. data/spec/fedex/signature_spec.rb +11 -0
  62. data/spec/fedex/spec_helper.rb +84 -0
  63. data/spec/fedex/track_spec.rb +37 -0
  64. data/spec/fedex_spec.rb +16 -0
  65. data/spec/lib/interceptor.rb +17 -0
  66. data/spec/proxy_spec.rb +42 -0
  67. data/spec/service_spec.rb +23 -0
  68. data/spec/spec_helper.rb +12 -0
  69. data/spec/ups/responses/blank.xml +0 -0
  70. data/spec/ups/responses/track_defaults.xml +2 -0
  71. data/spec/ups/spec_helper.rb +43 -0
  72. data/spec/ups_spec.rb +16 -0
  73. data/spec/validation_spec.rb +49 -0
  74. metadata +163 -0
@@ -0,0 +1,49 @@
1
+ require "builder"
2
+
3
+ module Shippinglogic
4
+ class UPS
5
+ # Methods relating to building and sending a request to UPS's web services.
6
+ module Request
7
+ private
8
+ # Convenience method for sending requests to UPS
9
+ def request(body)
10
+ real_class.post(base.url + real_class.path, :body => body)
11
+ end
12
+
13
+ # Convenience method to create a builder object so that our builder options are consistent across
14
+ # the various services.
15
+ #
16
+ # Ex: if I want to change the indent level to 3 it should change for all requests built.
17
+ def builder
18
+ b = Builder::XmlMarkup.new(:indent => 2)
19
+ b.instruct!
20
+ b
21
+ end
22
+
23
+ # A convenience method for building the authentication block in your XML request
24
+ def build_authentication(b)
25
+ b.AccessRequest(:"xml:lang" => "en-US") do
26
+ b.AccessLicenseNumber base.key
27
+ b.UserId base.account
28
+ b.Password base.password
29
+ end
30
+ end
31
+
32
+ # A convenience method for building the address block in your XML request
33
+ def build_address(b, type)
34
+ address_lines = send("#{type}_streets").to_s.split(/(?:\s*\n\s*)+/m, 3)
35
+
36
+ b.Address do
37
+ b.AddressLine1 address_lines[0] if address_lines[0]
38
+ b.AddressLine2 address_lines[1] if address_lines[1]
39
+ b.AddressLine3 address_lines[2] if address_lines[2]
40
+ b.City send("#{type}_city") if send("#{type}_city")
41
+ b.StateProvinceCode send("#{type}_state") if send("#{type}_state")
42
+ b.PostalCode send("#{type}_postal_code") if send("#{type}_postal_code")
43
+ b.CountryCode send("#{type}_country") if send("#{type}_country")
44
+ b.ResidentialAddressIndicator attribute_names.include?("#{type}_residential") && send("#{type}_residential")
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,58 @@
1
+ require "shippinglogic/ups/error"
2
+
3
+ module Shippinglogic
4
+ class UPS
5
+ # Methods relating to receiving a response from UPS and cleaning it up.
6
+ module Response
7
+ SUCCESSFUL_SEVERITIES = ["Warning"]
8
+
9
+ private
10
+ # Overwriting the request method to clean the response and handle errors.
11
+ def request(body)
12
+ response = clean_response(super)
13
+
14
+ if success?(response)
15
+ response
16
+ else
17
+ raise Error.new(body, response)
18
+ end
19
+ end
20
+
21
+ # Was the response a success?
22
+ def success?(response)
23
+ response.is_a?(Hash) && response[:response][:response_status_code] == "1"
24
+ end
25
+
26
+ # Cleans the response and returns it in a more 'user friendly' format that is easier
27
+ # to work with.
28
+ def clean_response(response)
29
+ cut_to_the_chase(sanitize_response_keys(response))
30
+ end
31
+
32
+ # UPS likes nested XML tags, because they send quite a bit of them back in responses.
33
+ # This method just 'cuts to the chase' and get to the heart of the response.
34
+ def cut_to_the_chase(response)
35
+ response.values.first
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
+ # Underscores and symbolizes incoming UPS response keys.
53
+ def sanitize_response_key(key)
54
+ key.gsub(/([a-z])([A-Z])/, '\1_\2').downcase.to_sym
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,11 @@
1
+ require "shippinglogic/ups/request"
2
+ require "shippinglogic/ups/response"
3
+
4
+ module Shippinglogic
5
+ class UPS
6
+ class Service < Shippinglogic::Service
7
+ include Request
8
+ include Response
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,53 @@
1
+ require "base64"
2
+
3
+ module Shippinglogic
4
+ class UPS
5
+ class ShipAccept < Service
6
+ def self.path
7
+ "/ShipAccept"
8
+ end
9
+
10
+ class Details
11
+ class Shipment; attr_accessor :tracking_number, :label; end
12
+
13
+ attr_accessor :rate, :currency, :shipments
14
+
15
+ def initialize(response)
16
+ details = response[:shipment_results]
17
+
18
+ charges = details[:shipment_charges][:total_charges]
19
+ self.rate = BigDecimal.new(charges[:monetary_value])
20
+ self.currency = charges[:currency_code]
21
+
22
+ self.shipments = [*details[:package_results]].collect do |package|
23
+ shipment = Shipment.new
24
+ shipment.tracking_number = package[:tracking_number]
25
+ shipment.label = Base64.decode64(package[:label_image][:graphic_image])
26
+ shipment
27
+ end
28
+ end
29
+ end
30
+
31
+ attribute :digest, :string
32
+
33
+ private
34
+ def target
35
+ @target ||= Details.new(request(build_request))
36
+ end
37
+
38
+ def build_request
39
+ b = builder
40
+ build_authentication(b)
41
+ b.instruct!
42
+
43
+ b.ShipmentAcceptRequest do
44
+ b.Request do
45
+ b.RequestAction "ShipAccept"
46
+ end
47
+
48
+ b.ShipmentDigest digest
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,170 @@
1
+ module Shippinglogic
2
+ class UPS
3
+ class ShipConfirm < Service
4
+ def self.path
5
+ "/ShipConfirm"
6
+ end
7
+
8
+ class Details
9
+ attr_accessor :digest, :tracking_number, :rate, :currency
10
+
11
+ def initialize(response)
12
+ self.digest = response[:shipment_digest]
13
+ self.tracking_number = response[:shipment_identification_number]
14
+
15
+ charges = response[:shipment_charges][:total_charges]
16
+ self.rate = BigDecimal.new(charges[:monetary_value])
17
+ self.currency = charges[:currency_code]
18
+ end
19
+ end
20
+
21
+ # shipper options
22
+ attribute :shipper_name, :string
23
+ attribute :shipper_phone_number, :string
24
+ attribute :shipper_email, :string
25
+ attribute :shipper_streets, :string
26
+ attribute :shipper_city, :string
27
+ attribute :shipper_state, :string
28
+ attribute :shipper_postal_code, :string
29
+ attribute :shipper_country, :string
30
+
31
+ # recipient options
32
+ attribute :recipient_name, :string
33
+ attribute :recipient_phone_number, :string
34
+ attribute :recipient_email, :string
35
+ attribute :recipient_streets, :string
36
+ attribute :recipient_city, :string
37
+ attribute :recipient_state, :string
38
+ attribute :recipient_postal_code, :string
39
+ attribute :recipient_country, :string
40
+ attribute :recipient_residential, :boolean, :default => false
41
+
42
+ # label options
43
+ attribute :label_format, :string, :default => "GIF"
44
+ attribute :label_file_type, :string, :default => "GIF"
45
+
46
+ # packaging options
47
+ attribute :packaging_type, :string, :default => "00"
48
+ attribute :package_count, :integer, :default => 1
49
+ attribute :package_weight, :float
50
+ attribute :package_weight_units, :string, :default => "LBS"
51
+ attribute :package_length, :integer
52
+ attribute :package_width, :integer
53
+ attribute :package_height, :integer
54
+ attribute :package_dimension_units, :string, :default => "IN"
55
+
56
+ # monetary options
57
+ attribute :currency_type, :string
58
+ attribute :insured_value, :decimal
59
+ attribute :payor_account_number, :string, :default => lambda { |shipment| shipment.base.number }
60
+
61
+ # delivery options
62
+ attribute :service_type, :string
63
+ #FIXME Setting the signature option to true raises and error. I believe this has something
64
+ # to do with UPS account-specific settings and signature service availability.
65
+ attribute :signature, :boolean, :default => false
66
+ attribute :saturday, :boolean, :default => false
67
+
68
+ private
69
+ def target
70
+ @target ||= Details.new(request(build_request))
71
+ end
72
+
73
+ def build_request
74
+ b = builder
75
+ build_authentication(b)
76
+ b.instruct!
77
+
78
+ b.ShipmentConfirmRequest do
79
+ b.Request do
80
+ b.RequestAction "ShipConfirm"
81
+ b.RequestOption "validate"
82
+ end
83
+
84
+ b.Shipment do
85
+ b.Shipper do
86
+ b.Name shipper_name
87
+ b.ShipperNumber payor_account_number
88
+ b.PhoneNumber shipper_phone_number
89
+ b.EMailAddress shipper_email
90
+ build_address(b, :shipper)
91
+ end
92
+
93
+ b.ShipTo do
94
+ b.CompanyName recipient_name
95
+ b.PhoneNumber recipient_phone_number
96
+ b.EMailAddress recipient_email
97
+ build_address(b, :recipient)
98
+ end
99
+
100
+ b.PaymentInformation do
101
+ b.Prepaid do
102
+ b.BillShipper do
103
+ b.AccountNumber payor_account_number
104
+ end
105
+ end
106
+ end
107
+
108
+ b.Service do
109
+ b.Code service_type
110
+ end
111
+
112
+ b.ShipmentServiceOptions do
113
+ b.SaturdayDelivery if saturday
114
+ if signature
115
+ b.DeliveryConfirmation do
116
+ b.DCISType "1"
117
+ end
118
+ end
119
+ end
120
+
121
+ package_count.times do |i|
122
+ b.Package do
123
+ b.PackagingType do
124
+ b.Code packaging_type
125
+ end
126
+
127
+ b.Dimensions do
128
+ b.UnitOfMeasurement do
129
+ b.Code package_dimension_units
130
+ end
131
+
132
+ b.Length "%.2f" % package_length
133
+ b.Width "%.2f" % package_width
134
+ b.Height "%.2f" % package_height
135
+ end
136
+
137
+ b.PackageWeight do
138
+ b.UnitOfMeasurement do
139
+ b.Code package_weight_units
140
+ end
141
+
142
+ b.Weight "%.1f" % package_weight
143
+ end
144
+
145
+ b.PackageServiceOptions do
146
+ if insured_value
147
+ b.InsuredValue do
148
+ b.MonetaryValue insured_value
149
+ b.CurrencyCode currency_type
150
+ end
151
+ end
152
+ end
153
+ end
154
+ end
155
+ end
156
+
157
+ b.LabelSpecification do
158
+ b.LabelPrintMethod do
159
+ b.Code label_file_type
160
+ end
161
+
162
+ b.LabelImageFormat do
163
+ b.Code label_format
164
+ end
165
+ end
166
+ end
167
+ end
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,118 @@
1
+ module Shippinglogic
2
+ class UPS
3
+ # An interface to the track services provided by UPS. 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
+ # ups = Shippinglogic::UPS.new(key, password, account)
15
+ # tracking_details = ups.track(:tracking_number => "my number")
16
+ #
17
+ # tracking_details.status
18
+ # # => "Delivered"
19
+ #
20
+ # tracking_details.signature_name
21
+ # # => "KKING"
22
+ #
23
+ # tracking_details.events.first
24
+ # # => #<Shippinglogic::UPS::Track::Event @postal_code="95817", @name="Delivered", @state="CA",
25
+ # # @city="Sacramento", @type="Delivered", @country="US", @occured_at=Mon Dec 08 10:43:37 -0500 2008>
26
+ #
27
+ # tracking_details.events.first.name
28
+ # # => "Delivered"
29
+ #
30
+ # === Note
31
+ #
32
+ # UPS does support locating packages through means other than a tracking number.
33
+ # These are not supported and probably won't be until someone needs them. It should
34
+ # be fairly simple to add, but I could not think of a reason why anyone would want to track
35
+ # a package with anything other than a tracking number.
36
+ class Track < Service
37
+ def self.path
38
+ "/Track"
39
+ end
40
+
41
+ class Details
42
+ # Each tracking result is an object of this class
43
+ class Event; attr_accessor :name, :type, :occured_at, :city, :state, :postal_code, :country; end
44
+
45
+ attr_accessor :origin_city, :origin_state, :origin_country,
46
+ :destination_city, :destination_state, :destination_country,
47
+ :signature_name, :service_type, :status, :delivery_at,
48
+ :events
49
+
50
+ def initialize(response)
51
+ details = response[:shipment]
52
+
53
+ if origin = details.fetch(:shipper, {})[:address]
54
+ self.origin_city = origin[:city]
55
+ self.origin_state = origin[:state_province_code]
56
+ self.origin_country = origin[:country_code]
57
+ end
58
+
59
+ if destination = details.fetch(:ship_to, {})[:address]
60
+ self.destination_city = destination[:city]
61
+ self.destination_state = destination[:state_province_code]
62
+ self.destination_country = destination[:country_code]
63
+ end
64
+
65
+ package = details[:package]
66
+ events = package[:activity].is_a?(Array) ? package[:activity] : [package[:activitiy]].compact
67
+ last_event = events.first
68
+ delivery = events.detect{|e| e[:status][:status_type][:code] == "D" }
69
+
70
+ self.signature_name = last_event && last_event[:signed_for_by_name]
71
+ self.service_type = details[:service][:description]
72
+ self.status = last_event && last_event[:status][:status_type][:description]
73
+ self.delivery_at = delivery && Time.parse(delivery[:date] + delivery[:time])
74
+
75
+ self.events = events.collect do |details|
76
+ event = Event.new
77
+ status = details[:status][:status_type]
78
+ event.name = status[:description]
79
+ event.type = status[:code]
80
+ #FIXME The proper spelling is "occurred", not "occured."
81
+ event.occured_at = Time.parse(details[:date] + details[:time])
82
+ location = details[:activity_location][:address]
83
+ event.city = location[:city]
84
+ event.state = location[:state_province_code]
85
+ event.postal_code = location[:postal_code]
86
+ event.country = location[:country_code]
87
+ event
88
+ end
89
+ end
90
+ end
91
+
92
+ attribute :tracking_number, :string
93
+
94
+ private
95
+ # The parent class Service requires that we define this method. This is our kicker. This method is only
96
+ # called when we need to deal with information from UPS. Notice the caching into the @target variable.
97
+ def target
98
+ @target ||= Details.new(request(build_request))
99
+ end
100
+
101
+ # Just building some XML to send off to UPS. UPS requires this particualar format.
102
+ def build_request
103
+ b = builder
104
+ build_authentication(b)
105
+ b.instruct!
106
+
107
+ b.TrackRequest do
108
+ b.Request do
109
+ b.RequestAction "Track"
110
+ b.RequestOption "activity"
111
+ end
112
+
113
+ b.TrackingNumber tracking_number
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end