corgibytes-tax_cloud 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.
Files changed (90) hide show
  1. checksums.yaml +17 -0
  2. data/.circleci/config.yml +81 -0
  3. data/.circleci/setup-rubygems.sh +9 -0
  4. data/.gitignore +67 -0
  5. data/.rubocop.yml +6 -0
  6. data/.rubocop_todo.yml +66 -0
  7. data/.travis.yml +7 -0
  8. data/CHANGELOG.rdoc +48 -0
  9. data/CONTRIBUTORS.txt +23 -0
  10. data/Dockerfile +9 -0
  11. data/Gemfile +22 -0
  12. data/LICENSE +23 -0
  13. data/README.md +190 -0
  14. data/Rakefile +29 -0
  15. data/docker-compose.yml +10 -0
  16. data/examples/.env.example +2 -0
  17. data/examples/lookup_example.rb +139 -0
  18. data/lib/config/locales/en.yml +34 -0
  19. data/lib/hash.rb +8 -0
  20. data/lib/savon_soap_xml.rb +32 -0
  21. data/lib/tasks/tax_cloud.rake +18 -0
  22. data/lib/tasks/tax_code_groups.rake +37 -0
  23. data/lib/tasks/tax_codes.rake +43 -0
  24. data/lib/tax_cloud.rb +70 -0
  25. data/lib/tax_cloud/address.rb +50 -0
  26. data/lib/tax_cloud/cart_item.rb +26 -0
  27. data/lib/tax_cloud/client.rb +60 -0
  28. data/lib/tax_cloud/configuration.rb +28 -0
  29. data/lib/tax_cloud/errors.rb +6 -0
  30. data/lib/tax_cloud/errors/api_error.rb +17 -0
  31. data/lib/tax_cloud/errors/missing_config_error.rb +13 -0
  32. data/lib/tax_cloud/errors/missing_config_option_error.rb +19 -0
  33. data/lib/tax_cloud/errors/soap_error.rb +30 -0
  34. data/lib/tax_cloud/errors/tax_cloud_error.rb +83 -0
  35. data/lib/tax_cloud/errors/unexpected_soap_response_error.rb +19 -0
  36. data/lib/tax_cloud/record.rb +14 -0
  37. data/lib/tax_cloud/responses.rb +13 -0
  38. data/lib/tax_cloud/responses/authorized.rb +10 -0
  39. data/lib/tax_cloud/responses/authorized_with_capture.rb +10 -0
  40. data/lib/tax_cloud/responses/base.rb +82 -0
  41. data/lib/tax_cloud/responses/captured.rb +10 -0
  42. data/lib/tax_cloud/responses/cart_item.rb +23 -0
  43. data/lib/tax_cloud/responses/generic.rb +31 -0
  44. data/lib/tax_cloud/responses/lookup.rb +38 -0
  45. data/lib/tax_cloud/responses/ping.rb +10 -0
  46. data/lib/tax_cloud/responses/returned.rb +10 -0
  47. data/lib/tax_cloud/responses/tax_code_groups.rb +30 -0
  48. data/lib/tax_cloud/responses/tax_codes.rb +30 -0
  49. data/lib/tax_cloud/responses/tax_codes_by_group.rb +30 -0
  50. data/lib/tax_cloud/responses/verify_address.rb +26 -0
  51. data/lib/tax_cloud/tax_code.rb +11 -0
  52. data/lib/tax_cloud/tax_code_constants.rb +560 -0
  53. data/lib/tax_cloud/tax_code_group.rb +28 -0
  54. data/lib/tax_cloud/tax_code_group_constants.rb +31 -0
  55. data/lib/tax_cloud/tax_code_groups.rb +25 -0
  56. data/lib/tax_cloud/tax_codes.rb +25 -0
  57. data/lib/tax_cloud/transaction.rb +118 -0
  58. data/lib/tax_cloud/version.rb +4 -0
  59. data/tax_cloud.gemspec +23 -0
  60. data/test/cassettes/authorized.yml +826 -0
  61. data/test/cassettes/authorized_with_capture.yml +826 -0
  62. data/test/cassettes/authorized_with_localized_time.yml +826 -0
  63. data/test/cassettes/captured.yml +872 -0
  64. data/test/cassettes/get_tic_groups.yml +783 -0
  65. data/test/cassettes/get_tics.yml +1079 -0
  66. data/test/cassettes/get_tics_by_group.yml +776 -0
  67. data/test/cassettes/invalid_soap_call.yml +778 -0
  68. data/test/cassettes/lookup.yml +780 -0
  69. data/test/cassettes/lookup_ny.yml +780 -0
  70. data/test/cassettes/ping.yml +776 -0
  71. data/test/cassettes/ping_with_invalid_credentials.yml +774 -0
  72. data/test/cassettes/ping_with_invalid_response.yml +774 -0
  73. data/test/cassettes/returned.yml +872 -0
  74. data/test/cassettes/verify_bad_address.yml +772 -0
  75. data/test/cassettes/verify_good_address.yml +772 -0
  76. data/test/helper.rb +17 -0
  77. data/test/test_address.rb +54 -0
  78. data/test/test_cart_item.rb +15 -0
  79. data/test/test_client.rb +27 -0
  80. data/test/test_configuration_optional_keys.rb +42 -0
  81. data/test/test_configuration_required_keys.rb +31 -0
  82. data/test/test_lookup_response.rb +20 -0
  83. data/test/test_setup.rb +17 -0
  84. data/test/test_soap.rb +11 -0
  85. data/test/test_tax_code_groups.rb +29 -0
  86. data/test/test_tax_codes.rb +17 -0
  87. data/test/test_transaction.rb +78 -0
  88. data/test/test_transaction_ny.rb +25 -0
  89. data/test/vcr_setup.rb +9 -0
  90. metadata +174 -0
@@ -0,0 +1,26 @@
1
+ module TaxCloud #:nodoc:
2
+ # A <tt>CartItem</tt> defines a single line item for the purchase. Used to calculate the tax amount.
3
+ class CartItem < Record
4
+ # The unique index number for the line item. Must be unique for the scope of the cart.
5
+ attr_accessor :index
6
+ # The stock keeping unit (SKU) number.
7
+ attr_accessor :item_id
8
+ # The taxable information code. See TaxCloud::TaxCodes.
9
+ attr_accessor :tic
10
+ # The price of the item. All prices are USD. Do not include currency symbol.
11
+ attr_accessor :price
12
+ # The total number of items.
13
+ attr_accessor :quantity
14
+
15
+ # Convert the object to a usable hash for SOAP requests.
16
+ def to_hash
17
+ {
18
+ 'Index' => index,
19
+ 'ItemID' => item_id,
20
+ 'TIC' => tic,
21
+ 'Price' => price,
22
+ 'Qty' => quantity
23
+ }
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,60 @@
1
+ module TaxCloud #:nodoc:
2
+ # A <tt>Client</tt> communicates with the TaxCloud service.
3
+ class Client < Savon::Client
4
+ # Create a new client.
5
+ def initialize
6
+ super TaxCloud::WSDL_URL
7
+
8
+ if client_params.key?(:read_timeout)
9
+ http.read_timeout = client_params[:read_timeout]
10
+ end
11
+
12
+ if client_params.key?(:open_timeout)
13
+ http.open_timeout = client_params[:open_timeout]
14
+ end
15
+ end
16
+
17
+ # Make a safe SOAP call.
18
+ # Will raise a TaxCloud::Errors::SoapError on error.
19
+ #
20
+ # === Parameters
21
+ # [method] SOAP method.
22
+ # [body] Body content.
23
+ def request(method, body = {})
24
+ safe do
25
+ super method, :body => body.merge(auth_params)
26
+ end
27
+ end
28
+
29
+ # Ping the TaxCloud service.
30
+ #
31
+ # Returns "OK" or raises an error if the TaxCloud service is unreachable.
32
+ def ping
33
+ TaxCloud::Responses::Ping.parse request(:ping)
34
+ end
35
+
36
+ private
37
+
38
+ # Authorization hash to use with all SOAP requests
39
+ def auth_params
40
+ return {} unless TaxCloud.configuration
41
+ {
42
+ 'apiLoginID' => TaxCloud.configuration.api_login_id,
43
+ 'apiKey' => TaxCloud.configuration.api_key
44
+ }
45
+ end
46
+
47
+ def client_params
48
+ { wsdl: TaxCloud::WSDL_URL }.tap do |params|
49
+ params[:open_timeout] = TaxCloud.configuration.open_timeout if TaxCloud.configuration.open_timeout
50
+ params[:read_timeout] = TaxCloud.configuration.read_timeout if TaxCloud.configuration.read_timeout
51
+ end
52
+ end
53
+
54
+ def safe
55
+ yield
56
+ rescue Savon::SOAP::Fault => e
57
+ raise TaxCloud::Errors::SoapError.new(e)
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,28 @@
1
+ module TaxCloud #:nodoc:
2
+ # TaxCloud gem configuration.
3
+ class Configuration
4
+ # TaxCloud login ID.
5
+ attr_accessor :api_login_id
6
+ # TaxCloud API key.
7
+ attr_accessor :api_key
8
+ # Optional USPS username.
9
+ attr_accessor :usps_username
10
+ # Savon client option open_timeout.
11
+ attr_accessor :open_timeout
12
+ # Savon client option read_timeout.
13
+ attr_accessor :read_timeout
14
+
15
+ def initialize
16
+ @open_timeout = 2
17
+ @read_timeout = 2
18
+ end
19
+
20
+ # Check the configuration.
21
+ #
22
+ # Will raise a TaxCloud::Errors::MissingConfigOption if any of the API login ID or the API key are missing.
23
+ def check!
24
+ raise TaxCloud::Errors::MissingConfigOption.new('api_login_id') unless api_login_id && !api_login_id.strip.empty?
25
+ raise TaxCloud::Errors::MissingConfigOption.new('api_key') unless api_key && !api_key.strip.empty?
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,6 @@
1
+ require 'tax_cloud/errors/tax_cloud_error'
2
+ require 'tax_cloud/errors/missing_config_error'
3
+ require 'tax_cloud/errors/missing_config_option_error'
4
+ require 'tax_cloud/errors/soap_error'
5
+ require 'tax_cloud/errors/unexpected_soap_response_error'
6
+ require 'tax_cloud/errors/api_error'
@@ -0,0 +1,17 @@
1
+
2
+ module TaxCloud #:nodoc:
3
+ module Errors #:nodoc:
4
+ # This error is raised when the TaxCloud service
5
+ # returns an error from an API.
6
+ class ApiError < TaxCloudError
7
+ # === Parameters
8
+ # [message] Error message.
9
+ # [raw] Raw data from the SOAP response.
10
+ def initialize(message, raw)
11
+ super(compose_message('api_error',
12
+ message: message,
13
+ raw: raw))
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,13 @@
1
+
2
+ module TaxCloud #:nodoc:
3
+ module Errors #:nodoc:
4
+ # This error is raised when attempting to create a new client
5
+ # without configuring TaxCloud.
6
+ class MissingConfig < TaxCloudError
7
+ # Create a new error.
8
+ def initialize
9
+ super(compose_message('missing_config'))
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,19 @@
1
+
2
+ module TaxCloud #:nodoc:
3
+ module Errors #:nodoc:
4
+ # This error is raised when a configuration
5
+ # option is missing.
6
+ class MissingConfigOption < TaxCloudError
7
+ # === Parameters
8
+ # [name] The attempted config option name.
9
+ def initialize(name)
10
+ super(
11
+ compose_message(
12
+ 'missing_config_option',
13
+ name: name
14
+ )
15
+ )
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,30 @@
1
+
2
+ module TaxCloud #:nodoc:
3
+ module Errors #:nodoc:
4
+ # This error is raised when a SOAP call fails.
5
+ class SoapError < TaxCloudError
6
+ # Original SOAP failt.
7
+ attr_reader :fault
8
+
9
+ # Create the new error.
10
+ # === Parameters
11
+ # [e] SOAP response.
12
+ def initialize(e)
13
+ @fault = e
14
+ e.to_hash.tap do |fault|
15
+ fault_code = fault[:fault][:faultcode]
16
+ fault_string = parse_fault(fault[:fault][:faultstring])
17
+ super(compose_message('soap_error',
18
+ message: fault_string,
19
+ code: fault_code))
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def parse_fault(fault_string)
26
+ fault_string.lines.first.strip
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,83 @@
1
+
2
+ module TaxCloud #:nodoc:
3
+ module Errors #:nodoc:
4
+ # Default parent TaxCloud error for all custom errors. This handles the base
5
+ # key for the translations and provides the convenience method for
6
+ # translating the messages.
7
+ #
8
+ # Generously borrowed from Mongoid[https://github.com/mongoid/mongoid/blob/master/lib/mongoid/errors/mongoid_error.rb].
9
+ class TaxCloudError < StandardError
10
+ # Problem occurred.
11
+ attr_reader :problem
12
+
13
+ # Summary of the problem.
14
+ attr_reader :summary
15
+
16
+ # Suggested problem resolution.
17
+ attr_reader :resolution
18
+
19
+ # Compose the message.
20
+ # === Parameters
21
+ # [key] Lookup key in the translation table.
22
+ # [attributes] The objects to pass to create the message.
23
+ def compose_message(key, attributes = {})
24
+ @problem = create_problem(key, attributes)
25
+ @summary = create_summary(key, attributes)
26
+ @resolution = create_resolution(key, attributes)
27
+
28
+ "\nProblem:\n #{@problem}" \
29
+ "\nSummary:\n #{@summary}" \
30
+ "\nResolution:\n #{@resolution}"
31
+ end
32
+
33
+ private
34
+
35
+ BASE_KEY = 'taxcloud.errors.messages'.freeze #:nodoc:
36
+
37
+ # Given the key of the specific error and the options hash, translate the
38
+ # message.
39
+ #
40
+ # === Parameters
41
+ # [key] The key of the error in the locales.
42
+ # [options] The objects to pass to create the message.
43
+ #
44
+ # Returns a localized error message string.
45
+ def translate(key, options)
46
+ ::I18n.translate("#{BASE_KEY}.#{key}", { locale: :en }.merge(options)).strip
47
+ end
48
+
49
+ # Create the problem.
50
+ #
51
+ # === Parameters
52
+ # [key] The error key.
53
+ # [attributes] The attributes to interpolate.
54
+ #
55
+ # Returns the problem.
56
+ def create_problem(key, attributes)
57
+ translate("#{key}.message", attributes)
58
+ end
59
+
60
+ # Create the summary.
61
+ #
62
+ # === Parameters
63
+ # [key] The error key.
64
+ # [attributes] The attributes to interpolate.
65
+ #
66
+ # Returns the summary.
67
+ def create_summary(key, attributes)
68
+ translate("#{key}.summary", attributes)
69
+ end
70
+
71
+ # Create the resolution.
72
+ #
73
+ # === Parameters
74
+ # [key] The error key.
75
+ # [attributes] The attributes to interpolate.
76
+ #
77
+ # Returns the resolution.
78
+ def create_resolution(key, attributes)
79
+ translate("#{key}.resolution", attributes)
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,19 @@
1
+
2
+ module TaxCloud #:nodoc:
3
+ module Errors #:nodoc:
4
+ # This error is raised when TaxCloud returns an
5
+ # unexpected SOAP response.
6
+ class UnexpectedSoapResponse < TaxCloudError
7
+ # === Parameters
8
+ # [raw] Raw data from the SOAP response.
9
+ # [key] Expected key in the SOAP response.
10
+ # [chain] Complete SOAP response chain in which the key could not be found.
11
+ def initialize(raw, key, chain)
12
+ super(compose_message('unexpected_soap_response',
13
+ key: key,
14
+ raw: raw,
15
+ chain: chain))
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,14 @@
1
+ module TaxCloud #:nodoc:
2
+ # A generic TaxCloud record.
3
+ class Record
4
+ # Initialize the record.
5
+ #
6
+ # === Parameters
7
+ # [attrs] Attributes defined for this record.
8
+ def initialize(attrs = {})
9
+ attrs.each do |sym, val|
10
+ send "#{sym}=", val
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,13 @@
1
+ require 'tax_cloud/responses/base'
2
+ require 'tax_cloud/responses/generic'
3
+ require 'tax_cloud/responses/ping'
4
+ require 'tax_cloud/responses/verify_address'
5
+ require 'tax_cloud/responses/cart_item'
6
+ require 'tax_cloud/responses/lookup'
7
+ require 'tax_cloud/responses/authorized'
8
+ require 'tax_cloud/responses/authorized_with_capture'
9
+ require 'tax_cloud/responses/captured'
10
+ require 'tax_cloud/responses/returned'
11
+ require 'tax_cloud/responses/tax_codes'
12
+ require 'tax_cloud/responses/tax_code_groups'
13
+ require 'tax_cloud/responses/tax_codes_by_group'
@@ -0,0 +1,10 @@
1
+ module TaxCloud #:nodoc:
2
+ module Responses #:nodoc:
3
+ # Response to a TaxCloud Authorized API call.
4
+ #
5
+ # See https://api.taxcloud.net/1.0/TaxCloud.asmx?op=Authorized.
6
+ class Authorized < Generic
7
+ response_key :authorized
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ module TaxCloud #:nodoc:
2
+ module Responses #:nodoc:
3
+ # Response to a TaxCloud AuthorizedWithCapture API call.
4
+ #
5
+ # See https://api.taxcloud.net/1.0/TaxCloud.asmx?op=AuthorizedWithCapture.
6
+ class AuthorizedWithCapture < Generic
7
+ response_key :authorized_with_capture
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,82 @@
1
+ module TaxCloud #:nodoc:
2
+ module Responses #:nodoc:
3
+ # A base TaxCloud SOAP response.
4
+ class Base
5
+ # Raw response.
6
+ attr_accessor :raw
7
+
8
+ # === Parameters
9
+ # [savon_response] Response from a SOAP API call.
10
+ def initialize(savon_response)
11
+ @raw = savon_response.to_hash
12
+ parse!
13
+ end
14
+
15
+ class << self
16
+ # === Parameters
17
+ # [value] Location of the response type in the SOAP XML.
18
+ def response_type(value)
19
+ set_dsl(:response_type, value)
20
+ end
21
+
22
+ # === Parameters
23
+ # [value] Location of the error message in the SOAP XML.
24
+ def error_message(value)
25
+ set_dsl(:error_message, value)
26
+ end
27
+
28
+ # === Parameters
29
+ # [value] Location of the error number in the SOAP XML.
30
+ def error_number(value)
31
+ set_dsl(:error_number, value)
32
+ end
33
+
34
+ # Parse a SOAP response.
35
+ #
36
+ # === Parameters
37
+ # [savon_response] SOAP response.
38
+ def parse(savon_response)
39
+ new(savon_response)
40
+ end
41
+ end
42
+
43
+ # Match an element in the SOAP response.
44
+ #
45
+ # === Parameters
46
+ # [chain] XML path to match.
47
+ def match(chain)
48
+ current_value = raw
49
+ chain.split('/').each do |key|
50
+ current_value = current_value[key.to_sym]
51
+ next if current_value
52
+ raise TaxCloud::Errors::UnexpectedSoapResponse.new(raw, key, chain)
53
+ end
54
+ current_value
55
+ end
56
+
57
+ private
58
+
59
+ class_attribute :dsl
60
+
61
+ class << self
62
+ def set_dsl(key, value)
63
+ self.dsl ||= {}
64
+ self.dsl[key] = value
65
+ self.dsl
66
+ end
67
+ end
68
+
69
+ def parse!
70
+ if self.dsl[:response_type]
71
+ case match(self.dsl[:response_type])
72
+ when 'OK', 'Informational' then
73
+ return true
74
+ end
75
+ elsif self.dsl[:error_number]
76
+ return true if match(self.dsl[:error_number]) == '0'
77
+ end
78
+ raise TaxCloud::Errors::ApiError.new(match(self.dsl[:error_message]), raw) if self.dsl[:error_message]
79
+ end
80
+ end
81
+ end
82
+ end