corgibytes-tax_cloud 0.9.0

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