ups-ruby 0.8.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 (56) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +22 -0
  3. data/.hound.yml +2 -0
  4. data/.rubocop.yml +1064 -0
  5. data/.travis.yml +10 -0
  6. data/Gemfile +10 -0
  7. data/Gemfile.lock +46 -0
  8. data/LICENSE.txt +14 -0
  9. data/README.md +78 -0
  10. data/Rakefile +17 -0
  11. data/lib/ups-ruby.rb +2 -0
  12. data/lib/ups.rb +33 -0
  13. data/lib/ups/builders/address_builder.rb +135 -0
  14. data/lib/ups/builders/builder_base.rb +216 -0
  15. data/lib/ups/builders/organisation_builder.rb +74 -0
  16. data/lib/ups/builders/rate_builder.rb +21 -0
  17. data/lib/ups/builders/ship_accept_builder.rb +30 -0
  18. data/lib/ups/builders/ship_confirm_builder.rb +103 -0
  19. data/lib/ups/builders/shipper_builder.rb +88 -0
  20. data/lib/ups/connection.rb +124 -0
  21. data/lib/ups/data.rb +50 -0
  22. data/lib/ups/data/canadian_states.rb +21 -0
  23. data/lib/ups/data/ie_counties.rb +10 -0
  24. data/lib/ups/data/ie_county_prefixes.rb +15 -0
  25. data/lib/ups/data/us_states.rb +59 -0
  26. data/lib/ups/exceptions.rb +7 -0
  27. data/lib/ups/packaging.rb +27 -0
  28. data/lib/ups/parsers/parser_base.rb +48 -0
  29. data/lib/ups/parsers/rates_parser.rb +60 -0
  30. data/lib/ups/parsers/ship_accept_parser.rb +52 -0
  31. data/lib/ups/parsers/ship_confirm_parser.rb +16 -0
  32. data/lib/ups/services.rb +21 -0
  33. data/lib/ups/version.rb +10 -0
  34. data/spec/spec_helper.rb +18 -0
  35. data/spec/stubs/rates_negotiated_success.xml +227 -0
  36. data/spec/stubs/rates_success.xml +196 -0
  37. data/spec/stubs/ship_accept_failure.xml +12 -0
  38. data/spec/stubs/ship_accept_success.xml +56 -0
  39. data/spec/stubs/ship_confirm_failure.xml +12 -0
  40. data/spec/stubs/ship_confirm_success.xml +50 -0
  41. data/spec/support/RateRequest.xsd +1 -0
  42. data/spec/support/ShipAcceptRequest.xsd +36 -0
  43. data/spec/support/ShipConfirmRequest.xsd +996 -0
  44. data/spec/support/schema_path.rb +5 -0
  45. data/spec/support/shipping_options.rb +48 -0
  46. data/spec/support/xsd_validator.rb +11 -0
  47. data/spec/ups/builders/address_builder_spec.rb +97 -0
  48. data/spec/ups/builders/rate_builder_spec.rb +20 -0
  49. data/spec/ups/builders/ship_accept_builder_spec.rb +16 -0
  50. data/spec/ups/builders/ship_confirm_builder_spec.rb +23 -0
  51. data/spec/ups/connection/rates_negotiated_spec.rb +69 -0
  52. data/spec/ups/connection/rates_standard_spec.rb +71 -0
  53. data/spec/ups/connection/ship_spec.rb +111 -0
  54. data/spec/ups/connection_spec.rb +20 -0
  55. data/ups.gemspec +24 -0
  56. metadata +166 -0
@@ -0,0 +1,74 @@
1
+ require 'ox'
2
+
3
+ module UPS
4
+ module Builders
5
+ # The {OrganisationBuilder} class builds UPS XML Organization Objects.
6
+ #
7
+ # @author Paul Trippett
8
+ # @since 0.1.0
9
+ # @attr [String] name The Containing XML Element Name
10
+ # @attr [Hash] opts The Organization and Address Parts
11
+ class OrganisationBuilder < BuilderBase
12
+ include Ox
13
+
14
+ attr_accessor :name, :opts
15
+
16
+ # Initializes a new {AddressBuilder} object
17
+ #
18
+ # @param [Hash] opts The Organization and Address Parts
19
+ # @option opts [String] :company_name Company Name
20
+ # @option opts [String] :phone_number Phone Number
21
+ # @option opts [String] :address_line_1 Address Line 1
22
+ # @option opts [String] :city City
23
+ # @option opts [String] :state State
24
+ # @option opts [String] :postal_code Zip or Postal Code
25
+ # @option opts [String] :country Country
26
+ def initialize(name, opts = {})
27
+ self.name = name
28
+ self.opts = opts
29
+ end
30
+
31
+ # Returns an XML representation of company_name
32
+ #
33
+ # @return [Ox::Element] XML representation of company_name
34
+ def company_name
35
+ element_with_value('CompanyName', opts[:company_name][0..34])
36
+ end
37
+
38
+ # Returns an XML representation of phone_number
39
+ #
40
+ # @return [Ox::Element] XML representation of phone_number
41
+ def phone_number
42
+ element_with_value('PhoneNumber', opts[:phone_number][0..14])
43
+ end
44
+
45
+ # Returns an XML representation of AttentionName for which we use company
46
+ # name
47
+ #
48
+ # @return [Ox::Element] XML representation of company_name part
49
+ def attention_name
50
+ element_with_value('AttentionName', opts[:attention_name][0..34])
51
+ end
52
+
53
+ # Returns an XML representation of address
54
+ #
55
+ # @return [Ox::Element] An instance of {AddressBuilder} containing the
56
+ # address
57
+ def address
58
+ AddressBuilder.new(opts).to_xml
59
+ end
60
+
61
+ # Returns an XML representation of a UPS Organization
62
+ #
63
+ # @return [Ox::Element] XML representation of the current object
64
+ def to_xml
65
+ Element.new(name).tap do |org|
66
+ org << company_name
67
+ org << phone_number
68
+ org << attention_name
69
+ org << address
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,21 @@
1
+ require 'ox'
2
+
3
+ module UPS
4
+ module Builders
5
+ # The {RateBuilder} class builds UPS XML Rate Objects.
6
+ #
7
+ # @author Paul Trippett
8
+ # @since 0.1.0
9
+ class RateBuilder < BuilderBase
10
+ include Ox
11
+
12
+ # Initializes a new {RateBuilder} object
13
+ #
14
+ def initialize
15
+ super 'RatingServiceSelectionRequest'
16
+
17
+ add_request('Rate', 'Shop')
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,30 @@
1
+ require 'ox'
2
+
3
+ module UPS
4
+ module Builders
5
+ # The {ShipAcceptBuilder} class builds UPS XML ShipAccept Objects.
6
+ #
7
+ # @author Paul Trippett
8
+ # @since 0.1.0
9
+ class ShipAcceptBuilder < BuilderBase
10
+ include Ox
11
+
12
+ # Initializes a new {ShipAcceptBuilder} object
13
+ #
14
+ def initialize
15
+ super 'ShipmentAcceptRequest'
16
+
17
+ add_request 'ShipAccept', '1'
18
+ end
19
+
20
+ # Adds a ShipmentDigest section to the XML document being built
21
+ #
22
+ # @param [String] digest The UPS Shipment Digest returned from the
23
+ # ShipConfirm request
24
+ # @return [void]
25
+ def add_shipment_digest(digest)
26
+ root << element_with_value('ShipmentDigest', digest)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,103 @@
1
+ require 'ox'
2
+
3
+ module UPS
4
+ module Builders
5
+ # The {ShipConfirmBuilder} class builds UPS XML ShipConfirm Objects.
6
+ #
7
+ # @author Paul Trippett
8
+ # @since 0.1.0
9
+ # @attr [String] name The Containing XML Element Name
10
+ # @attr [Hash] opts The Organization and Address Parts
11
+ class ShipConfirmBuilder < BuilderBase
12
+ include Ox
13
+
14
+ # Initializes a new {ShipConfirmBuilder} object
15
+ #
16
+ def initialize
17
+ super 'ShipmentConfirmRequest'
18
+
19
+ add_request 'ShipConfirm', 'validate'
20
+ end
21
+
22
+ # Adds a LabelSpecification section to the XML document being built
23
+ # according to user inputs
24
+ #
25
+ # @return [void]
26
+ def add_label_specification(format, size)
27
+ root << Element.new('LabelSpecification').tap do |label_spec|
28
+ label_spec << label_print_method(format)
29
+ label_spec << label_image_format(format)
30
+ label_spec << label_stock_size(size)
31
+ label_spec << http_user_agent if gif?(format)
32
+ end
33
+ end
34
+
35
+ # Adds a Service section to the XML document being built
36
+ #
37
+ # @param [String] service_code The Service code for the choosen Shipping
38
+ # method
39
+ # @param [optional, String] service_description A description for the
40
+ # choosen Shipping Method
41
+ # @return [void]
42
+ def add_service(service_code, service_description = '')
43
+ shipment_root << code_description('Service',
44
+ service_code,
45
+ service_description)
46
+ end
47
+
48
+ # Adds Description to XML document being built
49
+ #
50
+ # @param [String] description The description for goods being sent
51
+ #
52
+ # @return [void]
53
+ def add_description(description)
54
+ shipment_root << element_with_value('Description', description)
55
+ end
56
+
57
+ # Adds ReferenceNumber to the XML document being built
58
+ #
59
+ # @param [Hash] opts A Hash of data to build the requested section
60
+ # @option opts [String] :code Code
61
+ # @option opts [String] :value Value
62
+ #
63
+ # @return [void]
64
+ def add_reference_number(opts = {})
65
+ shipment_root << reference_number(opts[:code], opts[:value])
66
+ end
67
+
68
+ private
69
+
70
+ def gif?(string)
71
+ string.downcase == 'gif'
72
+ end
73
+
74
+ def http_user_agent
75
+ element_with_value('HTTPUserAgent', version_string)
76
+ end
77
+
78
+ def version_string
79
+ "RubyUPS/#{UPS::Version::STRING}"
80
+ end
81
+
82
+ def label_print_method(format)
83
+ code_description 'LabelPrintMethod', "#{format}", "#{format} file"
84
+ end
85
+
86
+ def label_image_format(format)
87
+ code_description 'LabelImageFormat', "#{format}", "#{format}"
88
+ end
89
+
90
+ def label_stock_size(size)
91
+ multi_valued('LabelStockSize',
92
+ 'Height' => size[:height].to_s,
93
+ 'Width' => size[:width].to_s)
94
+ end
95
+
96
+ def reference_number(code, value)
97
+ multi_valued('ReferenceNumber',
98
+ 'Code' => code.to_s,
99
+ 'Value' => value.to_s)
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,88 @@
1
+ require 'ox'
2
+
3
+ module UPS
4
+ module Builders
5
+ # The {ShipperBuilder} class builds UPS XML Organization Objects.
6
+ #
7
+ # @author Paul Trippett
8
+ # @since 0.1.0
9
+ # @attr [String] name The Containing XML Element Name
10
+ # @attr [Hash] opts The Shipper and Address Parts
11
+ class ShipperBuilder < BuilderBase
12
+ include Ox
13
+
14
+ attr_accessor :name, :opts
15
+
16
+ # Initializes a new {ShipperBuilder} object
17
+ #
18
+ # @param [Hash] opts The Shipper and Address Parts
19
+ # @option opts [String] :company_name Company Name
20
+ # @option opts [String] :phone_number Phone Number
21
+ # @option opts [String] :address_line_1 Address Line 1
22
+ # @option opts [String] :city City
23
+ # @option opts [String] :state State
24
+ # @option opts [String] :postal_code Zip or Postal Code
25
+ # @option opts [String] :country Country
26
+ def initialize(opts = {})
27
+ self.name = name
28
+ self.opts = opts
29
+ end
30
+
31
+ # Returns an XML representation of shipper_name
32
+ #
33
+ # @return [Ox::Element] XML representation of shipper_name
34
+ def shipper_name
35
+ element_with_value('Name', opts[:company_name])
36
+ end
37
+
38
+ # Returns an XML representation of company_name
39
+ #
40
+ # @return [Ox::Element] XML representation of company_name
41
+ def company_name
42
+ element_with_value('CompanyName', opts[:company_name])
43
+ end
44
+
45
+ # Returns an XML representation of company_name
46
+ #
47
+ # @return [Ox::Element] XML representation of phone_number
48
+ def phone_number
49
+ element_with_value('PhoneNumber', opts[:phone_number])
50
+ end
51
+
52
+ # Returns an XML representation of company_name
53
+ #
54
+ # @return [Ox::Element] XML representation of shipper_number
55
+ def shipper_number
56
+ element_with_value('ShipperNumber', opts[:shipper_number] || '')
57
+ end
58
+
59
+ # Returns an XML representation of the associated Address
60
+ #
61
+ # @return [Ox::Element] XML object of the associated Address
62
+ def address
63
+ AddressBuilder.new(opts).to_xml
64
+ end
65
+
66
+ # Returns an XML representation of attention_name
67
+ #
68
+ # @return [Ox::Element] XML representation of attention_name
69
+ def attention_name
70
+ element_with_value('AttentionName', opts[:attention_name] || '')
71
+ end
72
+
73
+ # Returns an XML representation of the current object
74
+ #
75
+ # @return [Ox::Element] XML representation of the current object
76
+ def to_xml
77
+ Element.new('Shipper').tap do |org|
78
+ org << shipper_name
79
+ org << attention_name
80
+ org << company_name
81
+ org << phone_number
82
+ org << shipper_number
83
+ org << address
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,124 @@
1
+ require 'uri'
2
+ require 'excon'
3
+ require 'digest/md5'
4
+ require 'ox'
5
+
6
+ module UPS
7
+ # The {Connection} class acts as the main entry point to performing rate and
8
+ # ship operations against the UPS API.
9
+ #
10
+ # @author Paul Trippett
11
+ # @abstract
12
+ # @since 0.1.0
13
+ # @attr [String] url The base url to use either TEST_URL or LIVE_URL
14
+ class Connection
15
+ attr_accessor :url
16
+
17
+ TEST_URL = 'https://wwwcie.ups.com'
18
+ LIVE_URL = 'https://onlinetools.ups.com'
19
+
20
+ RATE_PATH = '/ups.app/xml/Rate'
21
+ SHIP_CONFIRM_PATH = '/ups.app/xml/ShipConfirm'
22
+ SHIP_ACCEPT_PATH = '/ups.app/xml/ShipAccept'
23
+ ADDRESS_PATH = '/ups.app/xml/XAV'
24
+
25
+ DEFAULT_PARAMS = {
26
+ test_mode: false
27
+ }
28
+
29
+ # Initializes a new {Connection} object
30
+ #
31
+ # @param [Hash] params The initialization options
32
+ # @option params [Boolean] :test_mode If TEST_URL should be used for
33
+ # requests to the UPS URL
34
+ def initialize(params = {})
35
+ params = DEFAULT_PARAMS.merge(params)
36
+ self.url = (params[:test_mode]) ? TEST_URL : LIVE_URL
37
+ end
38
+
39
+ # Makes a request to fetch Rates for a shipment.
40
+ #
41
+ # A pre-configured {Builders::RateBuilder} object can be passed as the first
42
+ # option or a block yielded to configure a new {Builders::RateBuilder}
43
+ # object.
44
+ #
45
+ # @param [Builders::RateBuilder] rate_builder A pre-configured
46
+ # {Builders::RateBuilder} object to use
47
+ # @yield [rate_builder] A RateBuilder object for configuring
48
+ # the shipment information sent
49
+ def rates(rate_builder = nil)
50
+ if rate_builder.nil? && block_given?
51
+ rate_builder = UPS::Builders::RateBuilder.new
52
+ yield rate_builder
53
+ end
54
+
55
+ response = get_response_stream RATE_PATH, rate_builder.to_xml
56
+ UPS::Parsers::RatesParser.new.tap do |parser|
57
+ Ox.sax_parse(parser, response)
58
+ end
59
+ end
60
+
61
+ # Makes a request to ship a package
62
+ #
63
+ # A pre-configured {Builders::ShipConfirmBuilder} object can be passed as
64
+ # the first option or a block yielded to configure a new
65
+ # {Builders::ShipConfirmBuilder} object.
66
+ #
67
+ # @param [Builders::ShipConfirmBuilder] confirm_builder A pre-configured
68
+ # {Builders::ShipConfirmBuilder} object to use
69
+ # @yield [ship_confirm_builder] A ShipConfirmBuilder object for configuring
70
+ # the shipment information sent
71
+ def ship(confirm_builder = nil)
72
+ if confirm_builder.nil? && block_given?
73
+ confirm_builder = Builders::ShipConfirmBuilder.new
74
+ yield confirm_builder
75
+ end
76
+
77
+ confirm_response = make_confirm_request(confirm_builder)
78
+ return confirm_response unless confirm_response.success?
79
+
80
+ accept_builder = build_accept_request_from_confirm(confirm_builder,
81
+ confirm_response)
82
+ make_accept_request accept_builder
83
+ end
84
+
85
+ private
86
+
87
+ def build_url(path)
88
+ "#{url}#{path}"
89
+ end
90
+
91
+ def get_response_stream(path, body)
92
+ response = Excon.post(build_url(path), body: body)
93
+ StringIO.new(response.body)
94
+ end
95
+
96
+ def make_confirm_request(confirm_builder)
97
+ make_ship_request confirm_builder,
98
+ SHIP_CONFIRM_PATH,
99
+ Parsers::ShipConfirmParser.new
100
+ end
101
+
102
+ def make_accept_request(accept_builder)
103
+ make_ship_request accept_builder,
104
+ SHIP_ACCEPT_PATH,
105
+ Parsers::ShipAcceptParser.new
106
+ end
107
+
108
+ def make_ship_request(builder, path, ship_parser)
109
+ response = get_response_stream path, builder.to_xml
110
+ ship_parser.tap do |parser|
111
+ Ox.sax_parse(parser, response)
112
+ end
113
+ end
114
+
115
+ def build_accept_request_from_confirm(confirm_builder, confirm_response)
116
+ UPS::Builders::ShipAcceptBuilder.new.tap do |builder|
117
+ builder.add_access_request confirm_builder.license_number,
118
+ confirm_builder.user_id,
119
+ confirm_builder.password
120
+ builder.add_shipment_digest confirm_response.shipment_digest
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,50 @@
1
+ require 'levenshtein'
2
+
3
+ module UPS
4
+ module Data
5
+ class << self
6
+ EMPTY_STATE_MESSAGE = 'Invalid Address State [:state]'
7
+
8
+ # Normalizes Irish states as per UPS requirements
9
+ #
10
+ # @param [String] string The Irish State to normalize
11
+ # @return [String] The normalized Irish state name
12
+ def ie_state_normalizer(string)
13
+ string.tap do |target|
14
+ IE_COUNTY_PREFIXES.each do |prefix|
15
+ target.gsub!(/^#{Regexp.escape(prefix.downcase)} /i, '')
16
+ end
17
+ end
18
+ end
19
+
20
+ # Returns the closest matching Irish state name. Uses Levenshtein
21
+ # distance to correct any possible spelling errors.
22
+ #
23
+ # @param [String] match_string The Irish State to match
24
+ # @raise [InvalidAttributeError] If the passed match_String is nil or
25
+ # empty
26
+ # @return [String] The closest matching irish state with the specified
27
+ # name
28
+ def ie_state_matcher(match_string)
29
+ fail Exceptions::InvalidAttributeError, EMPTY_STATE_MESSAGE if
30
+ match_string.nil? || match_string.empty?
31
+
32
+ normalized_string = ie_state_normalizer string_normalizer match_string
33
+ counties_with_distances = IE_COUNTIES.map do |county|
34
+ [county, Levenshtein.distance(county.downcase, normalized_string)]
35
+ end
36
+ counties_with_distances_hash = Hash[*counties_with_distances.flatten]
37
+ counties_with_distances_hash.min_by { |_k, v| v }[0]
38
+ end
39
+
40
+ # Removes extra characters from a string
41
+ #
42
+ # @param [String] string The string to normalize and remove special
43
+ # characters
44
+ # @return [String] The normalized string
45
+ def string_normalizer(string)
46
+ string.downcase.gsub(/[^0-9a-z ]/i, '')
47
+ end
48
+ end
49
+ end
50
+ end