shippinglogic 1.2.3

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.
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