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
data/Rakefile ADDED
@@ -0,0 +1,29 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rake/testtask'
3
+ require 'rdoc/task'
4
+ require 'vcr'
5
+
6
+ Rake::TestTask.new(:test) do |test|
7
+ test.libs << 'lib' << 'test'
8
+ test.pattern = 'test/test_*.rb'
9
+ # REM
10
+ #test.verbose = false
11
+ end
12
+
13
+ RDoc::Task.new do |rd|
14
+ README = 'README.rdoc'.freeze
15
+ rd.main = README
16
+ rd.rdoc_files.include(README, 'CHANGELOG.rdoc', 'LICENSE.rdoc', 'lib/**/*.rb')
17
+ rd.rdoc_dir = 'doc'
18
+ rd.title = 'tax_cloud'
19
+ end
20
+
21
+ load 'lib/tasks/tax_cloud.rake'
22
+ load 'lib/tasks/tax_codes.rake'
23
+ load 'lib/tasks/tax_code_groups.rake'
24
+ load 'vcr/tasks/vcr.rake'
25
+
26
+ require 'rubocop/rake_task'
27
+ RuboCop::RakeTask.new(:rubocop)
28
+
29
+ task default: [:rubocop, :test]
@@ -0,0 +1,10 @@
1
+ version: '3.7'
2
+ services:
3
+ app:
4
+ build: .
5
+ volumes:
6
+ - .:/app
7
+ - bundler-data:/usr/local/bundle
8
+
9
+ volumes:
10
+ bundler-data:
@@ -0,0 +1,2 @@
1
+ TAX_CLOUD_LOGIN_ID=
2
+ TAX_CLOUD_API_KEY=
@@ -0,0 +1,139 @@
1
+ ##
2
+ # An example of doing a tax lookup that hits TaxCloud. To run:
3
+ #
4
+ # bundle exec ruby lookup_example.rb
5
+ #
6
+ # Make sure you copy the .env.example to .env an update the
7
+ # values in it before running. I also recommend no running
8
+ # this script against a live TaxCloud site. Run it only against
9
+ # test sites.
10
+ ##
11
+
12
+ require 'dotenv/load'
13
+ require 'securerandom'
14
+
15
+ require_relative '../lib/tax_cloud'
16
+
17
+ ##
18
+ # Load the TaxCloud settings from the environment file.\
19
+ #
20
+ def load_config
21
+ TaxCloud.configure do |config|
22
+ config.api_login_id = ENV['TAX_CLOUD_LOGIN_ID']
23
+ config.api_key = ENV['TAX_CLOUD_API_KEY']
24
+ config.open_timeout = 10
25
+ config.read_timeout = 10
26
+ end
27
+ end
28
+
29
+ ##
30
+ # Display the current TaxCloud settings.
31
+ #
32
+ def display_config
33
+ TaxCloud.configure do |config|
34
+ puts "API Login ID: #{config.api_login_id}"
35
+ puts "API Key: #{config.api_key}"
36
+ puts "Open timeout: #{config.open_timeout}"
37
+ puts "Read timeout: #{config.read_timeout}"
38
+ end
39
+ end
40
+
41
+ ##
42
+ # Create a test transaction with one cart item.
43
+ #
44
+ # Will create a new customer and cart each time using GUIDs for
45
+ # the customer and cart IDs. This prevents collisions with
46
+ # existing customer/carts when running the script multiple times.
47
+ #
48
+ def create_test_transaction
49
+ origin = TaxCloud::Address.new(
50
+ :address1 => '162 East Avenue',
51
+ :address2 => 'Third Floor',
52
+ :city => 'Norwalk',
53
+ :state => 'CT',
54
+ :zip5 => '06851')
55
+
56
+ destination = TaxCloud::Address.new(
57
+ :address1 => '3121 West Government Way',
58
+ :address2 => 'Suite 2B',
59
+ :city => 'Seattle',
60
+ :state => 'WA',
61
+ :zip5 => '98199')
62
+
63
+ transaction = TaxCloud::Transaction.new(
64
+ :customer_id => SecureRandom.uuid,
65
+ :cart_id => SecureRandom.uuid,
66
+ :origin => origin,
67
+ :destination => destination)
68
+
69
+ transaction.cart_items << TaxCloud::CartItem.new(
70
+ :index => 0,
71
+ :item_id => 'SKU-100',
72
+ :tic => TaxCloud::TaxCodes::GENERAL,
73
+ :price => 10.00,
74
+ :quantity => 1)
75
+
76
+ transaction
77
+ end
78
+
79
+ ##
80
+ # Displays information about the transaction:
81
+ #
82
+ # - Customer ID
83
+ # - Cart ID
84
+ # - Origin Address
85
+ # - Destination Address
86
+ # - Cart Items
87
+ #
88
+ # @param [TaxCloud::Transaction] transaction
89
+ #
90
+ def display_transaction(transaction)
91
+ puts "Customer ID: #{transaction.customer_id}"
92
+ puts "Cart ID: #{transaction.cart_id}"
93
+ puts "Origin: #{transaction.origin.to_hash}"
94
+ puts "Destination: #{transaction.destination.to_hash}"
95
+ puts "Cart Items: #{transaction.cart_items.map(&:to_hash)}"
96
+ end
97
+
98
+ ##
99
+ # Display the total tax and tax for each cart item.
100
+ #
101
+ # @param [TaxCloud::Responses::Lookup] lookup
102
+ def display_tax(lookup)
103
+ puts "Total tax amount: #{lookup.tax_amount}"
104
+
105
+ lookup.cart_items.each do |cart_item|
106
+ puts "Tax cart index #{cart_item.cart_item_index}: #{cart_item.tax_amount}"
107
+ end
108
+ end
109
+
110
+
111
+ ##
112
+ # Run an example lookup
113
+ ##
114
+ puts 'Loading config information from .env file...'
115
+ load_config
116
+ puts 'Config loaded'
117
+ puts
118
+
119
+ puts 'The TaxCloud config is:'
120
+ display_config
121
+ puts
122
+
123
+ puts 'Checking config...'
124
+ TaxCloud.configuration.check!
125
+ puts 'Check passed.'
126
+ puts
127
+
128
+ puts 'Pinging TaxCloud server...'
129
+ puts TaxCloud.client.ping
130
+
131
+ puts 'Cart values:'
132
+ transaction = create_test_transaction
133
+ display_transaction(transaction)
134
+ puts
135
+
136
+ puts 'Tax lookup:'
137
+ lookup = transaction.lookup
138
+ display_tax(lookup)
139
+ puts
@@ -0,0 +1,34 @@
1
+ en:
2
+ taxcloud:
3
+ errors:
4
+ messages:
5
+ missing_config:
6
+ message: "Missing configuration."
7
+ summary: "TaxCloud requires an API login ID and key."
8
+ resolution: "Create a TaxCloud merchant account at http://www.taxcloud.net.
9
+ Add a website to your TaxCloud account. This will generate an API ID and
10
+ API Key that you will need to use the service. Configure the TaxCloud
11
+ gem. For example, add the following to `config/initializers/tax_cloud.rb`.\n
12
+ \_\_TaxCloud.configure do |config|\n
13
+ \_\_\_config.api_login_id = 'your_tax_cloud_api_login_id'\n
14
+ \_\_\_config.api_key = 'your_tax_cloud_api_key'\n
15
+ \_\_\_config.usps_username = 'your_usps_username' # optional\n
16
+ \_\_end\n"
17
+ missing_config_option:
18
+ message: "Missing configuration option: %{name}."
19
+ summary: "A configuration option was not provided when configuring TaxCloud."
20
+ resolution: "Review your `TaxCloud.configure` code. Are you using an environment
21
+ variable that has not been set?"
22
+ soap_error:
23
+ message: "%{message}"
24
+ summary: "The server returned a %{code} error."
25
+ resolution: "Review the parameters passed to the request."
26
+ unexpected_soap_response:
27
+ message: "Expected a value for `%{key}` in `%{chain}` in the TaxCloud response."
28
+ summary: "TaxCloud returned an unexpected response: `%{raw}`."
29
+ resolution: "This is possibly a bug in the tax_cloud gem.\n
30
+ Please report it in https://github.com/drewtempelmeyer/tax_cloud/issues."
31
+ api_error:
32
+ message: "%{message}"
33
+ summary: "The TaxCloud server returned an error in the response: `%{raw}`"
34
+ resolution: "Check the request parameters."
data/lib/hash.rb ADDED
@@ -0,0 +1,8 @@
1
+ class Hash #:nodoc: all
2
+ # Downcase the keys. Use this because <tt>TaxCloud::Address.verify</tt> requires downcased keys
3
+ def downcase_keys
4
+ each_with_object({}) do |(k, v), h|
5
+ h[k.downcase] = v
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,32 @@
1
+ # Hack Savon to work properly with arrays
2
+ module Savon #:nodoc:
3
+ module SOAP #:nodoc:
4
+ class XML #:nodoc:
5
+
6
+ private
7
+
8
+ old_add_namespaces_to_body = self.instance_method(:add_namespaces_to_body)
9
+
10
+ def add_namespaces_to_body(hash, path = [input[1].to_s])
11
+ return unless hash
12
+ return hash if hash.kind_of? Array
13
+ return hash.to_s unless hash.kind_of? Hash
14
+
15
+ hash.inject({}) do |newhash, (key, value)|
16
+ camelcased_key = Gyoku::XMLKey.create(key)
17
+ newpath = path + [camelcased_key]
18
+
19
+ if used_namespaces[newpath]
20
+ newhash.merge(
21
+ "#{used_namespaces[newpath]}:#{camelcased_key}" =>
22
+ add_namespaces_to_body(value, types[newpath] ? [types[newpath]] : newpath)
23
+ )
24
+ else
25
+ newhash.merge(key => value)
26
+ end
27
+ end
28
+ end
29
+
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,18 @@
1
+ require 'tax_cloud'
2
+
3
+ namespace :tax_cloud do
4
+ desc 'Configure TaxCloud.'
5
+ task :configure do
6
+ unless TaxCloud.configured?
7
+ TaxCloud.configure do |config|
8
+ config.api_login_id = ENV['TAXCLOUD_API_LOGIN_ID']
9
+ config.api_key = ENV['TAXCLOUD_API_KEY']
10
+ config.usps_username = ENV['TAXCLOUD_USPS_USERNAME']
11
+ end
12
+ Savon.configure do |config|
13
+ config.log = false
14
+ end
15
+ HTTPI.log = false
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,37 @@
1
+ namespace :tax_cloud do
2
+ desc 'Generate tax code groups.'
3
+ task tax_code_groups: :configure do
4
+ filename = 'lib/tax_cloud/tax_code_group_constants.rb'
5
+
6
+ begin
7
+ groups = TaxCloud::TaxCode::Groups.all.values
8
+
9
+ File.open filename, 'wt' do |f|
10
+ f.write "module TaxCloud\n"
11
+ f.write " class TaxCode\n"
12
+ f.write " # Tax Code Groups.\n"
13
+ f.write " class Groups\n"
14
+
15
+ groups.each do |group|
16
+ puts " #{group.description}"
17
+ code = group.description.upcase
18
+ code.gsub! /[^A-Z0-9]/, '_'
19
+ code.gsub! /_$/, ''
20
+ code.gsub! /\_+/, '_'
21
+ f.write " \# #{group.description}\n"
22
+ f.write " #{code} = #{group.group_id}\n"
23
+ end
24
+
25
+ f.write " end\n"
26
+ f.write " end\n"
27
+ f.write "end\n"
28
+ end
29
+
30
+ puts "Done, #{filename}."
31
+ rescue => e
32
+ puts 'ERROR: Unable to generate a new list of tax codes.'
33
+ puts e.message
34
+ raise e
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,43 @@
1
+ namespace :tax_cloud do
2
+ desc 'Generate tax codes.'
3
+ task tax_codes: :configure do
4
+ filename = 'lib/tax_cloud/tax_code_constants.rb'
5
+
6
+ begin
7
+ groups = TaxCloud::TaxCode::Groups.all.values
8
+ groups_and_tax_codes = Hash[groups.map do |group|
9
+ [group, group.tax_codes.values]
10
+ end]
11
+ File.open filename, 'wt' do |f|
12
+ f.write "module TaxCloud\n"
13
+ f.write " # Tax Codes.\n"
14
+ f.write " class TaxCodes\n"
15
+ f.write "\n"
16
+ codes = {}
17
+ groups_and_tax_codes.each_pair do |group, tax_codes|
18
+ puts " #{group.description}"
19
+ f.write " \# #{group.description}\n\n"
20
+ tax_codes.each do |tax_code|
21
+ code = tax_code.description.upcase
22
+ code.gsub! /[^A-Z0-9]/, '_'
23
+ code.gsub! /\_+$/, ''
24
+ code.gsub! /\_+/, '_'
25
+ # avoid duplicates
26
+ code_id = codes[code] ? "#{code}_#{codes[code]}" : code
27
+ codes[code] = (codes[code] || 0) + 1
28
+ f.write " \# #{group.description}: #{tax_code.description}\n"
29
+ f.write " #{code_id} = #{tax_code.ticid}\n"
30
+ end
31
+ f.write "\n"
32
+ end
33
+ f.write " end\n"
34
+ f.write "end\n"
35
+ end
36
+ puts "Done, #{filename}."
37
+ rescue => e
38
+ puts 'ERROR: Unable to generate a new list of tax codes.'
39
+ puts e.message
40
+ raise e
41
+ end
42
+ end
43
+ end
data/lib/tax_cloud.rb ADDED
@@ -0,0 +1,70 @@
1
+ require 'savon'
2
+ require 'i18n'
3
+ require 'hash'
4
+ require 'savon_soap_xml'
5
+
6
+ require 'active_support'
7
+ require 'active_support/core_ext'
8
+
9
+ require 'tax_cloud/version'
10
+ require 'tax_cloud/errors'
11
+ require 'tax_cloud/responses'
12
+ require 'tax_cloud/record'
13
+ require 'tax_cloud/transaction'
14
+ require 'tax_cloud/address'
15
+ require 'tax_cloud/cart_item'
16
+ require 'tax_cloud/tax_code'
17
+ require 'tax_cloud/tax_code_group'
18
+ require 'tax_cloud/tax_codes'
19
+ require 'tax_cloud/tax_code_constants'
20
+ require 'tax_cloud/tax_code_groups'
21
+ require 'tax_cloud/tax_code_group_constants'
22
+ require 'tax_cloud/configuration'
23
+ require 'tax_cloud/client'
24
+
25
+ I18n.load_path << File.join(File.dirname(__FILE__), 'config', 'locales', 'en.yml')
26
+
27
+ # TaxCloud is a web service to calculate and track sales tax for your ecommerce platform. Integration is easy to use.
28
+ # For information on configuring and using the TaxCloud API, look at the <tt>README</tt> file.
29
+ module TaxCloud
30
+ # WSDL location for TaxCloud API.
31
+ WSDL_URL = 'https://api.taxcloud.net/1.0/?wsdl'.freeze
32
+
33
+ # TaxCloud API version.
34
+ API_VERSION = '1.0'.freeze
35
+
36
+ class << self
37
+ # TaxCloud gem configuration.
38
+ attr_accessor :configuration
39
+
40
+ # Returns true if the gem has been configured.
41
+ def configured?
42
+ configuration.present?
43
+ end
44
+
45
+ # Configure the gem.
46
+ def configure
47
+ self.configuration ||= Configuration.new
48
+ yield configuration
49
+ end
50
+
51
+ # Reset the current configuration.
52
+ def reset!
53
+ self.configuration = nil
54
+ @client = nil
55
+ end
56
+
57
+ # The configured SOAP client to the TaxCloud service.
58
+ def client
59
+ check_configuration!
60
+ @client ||= TaxCloud::Client.new
61
+ end
62
+
63
+ private
64
+
65
+ def check_configuration!
66
+ raise TaxCloud::Errors::MissingConfig.new unless self.configuration
67
+ self.configuration.check!
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,50 @@
1
+ module TaxCloud #:nodoc:
2
+ # An <tt>Address</tt> defines an address in the United States.
3
+ class Address < Record
4
+ # First line of address.
5
+ attr_accessor :address1
6
+ # Second line of adress.
7
+ attr_accessor :address2
8
+ # City.
9
+ attr_accessor :city
10
+ # State.
11
+ attr_accessor :state
12
+ # 5-digit Zip Code.
13
+ attr_accessor :zip5
14
+ # 4-digit Zip Code.
15
+ attr_accessor :zip4
16
+
17
+ # Verify this address.
18
+ #
19
+ # Returns a verified TaxCloud::Address.
20
+ def verify
21
+ params = to_hash.downcase_keys
22
+ if TaxCloud.configuration.usps_username
23
+ params = params.merge(
24
+ 'uspsUserID' => TaxCloud.configuration.usps_username
25
+ )
26
+ end
27
+ response = TaxCloud.client.request(:verify_address, params)
28
+ TaxCloud::Responses::VerifyAddress.parse(response)
29
+ end
30
+
31
+ # Complete zip code.
32
+ # Returns a 9-digit Zip Code, when available.
33
+ def zip
34
+ return nil unless zip5 && !zip5.empty?
35
+ [zip5, zip4].select { |z| z && !z.empty? }.join('-')
36
+ end
37
+
38
+ # Convert the object to a usable hash for SOAP requests
39
+ def to_hash
40
+ {
41
+ 'Address1' => address1,
42
+ 'Address2' => address2,
43
+ 'City' => city,
44
+ 'State' => state,
45
+ 'Zip5' => zip5,
46
+ 'Zip4' => zip4
47
+ }
48
+ end
49
+ end
50
+ end