ups-ruby 0.8.3

Sign up to get free protection for your applications and to get access to all the features.
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