suretax 0.1.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 (52) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +27 -0
  3. data/.travis.yml +14 -0
  4. data/Gemfile +4 -0
  5. data/Gemfile.lock +74 -0
  6. data/Gemfile.travis +12 -0
  7. data/LICENSE.txt +22 -0
  8. data/NOTES.md +3 -0
  9. data/README.md +58 -0
  10. data/Rakefile +1 -0
  11. data/circle.yml +7 -0
  12. data/lib/suretax.rb +24 -0
  13. data/lib/suretax/api.rb +7 -0
  14. data/lib/suretax/api/cancel_request.rb +64 -0
  15. data/lib/suretax/api/group.rb +16 -0
  16. data/lib/suretax/api/item_message.rb +13 -0
  17. data/lib/suretax/api/request.rb +133 -0
  18. data/lib/suretax/api/request_item.rb +84 -0
  19. data/lib/suretax/api/response.rb +53 -0
  20. data/lib/suretax/api/tax.rb +25 -0
  21. data/lib/suretax/api/tax_amount.rb +46 -0
  22. data/lib/suretax/concerns.rb +7 -0
  23. data/lib/suretax/concerns/validatable.rb +208 -0
  24. data/lib/suretax/configuration.rb +84 -0
  25. data/lib/suretax/connection.rb +48 -0
  26. data/lib/suretax/constants.rb +7 -0
  27. data/lib/suretax/constants/regulatory_codes.rb +11 -0
  28. data/lib/suretax/constants/response_groups.rb +8 -0
  29. data/lib/suretax/constants/sales_type_codes.rb +8 -0
  30. data/lib/suretax/constants/tax_situs_codes.rb +12 -0
  31. data/lib/suretax/constants/transaction_type_codes.rb +505 -0
  32. data/lib/suretax/response.rb +70 -0
  33. data/lib/suretax/version.rb +3 -0
  34. data/spec/lib/suretax/api/group_spec.rb +50 -0
  35. data/spec/lib/suretax/api/request_item_spec.rb +54 -0
  36. data/spec/lib/suretax/api/request_item_validations_spec.rb +237 -0
  37. data/spec/lib/suretax/api/request_spec.rb +197 -0
  38. data/spec/lib/suretax/api/request_validations_spec.rb +384 -0
  39. data/spec/lib/suretax/api/response_spec.rb +165 -0
  40. data/spec/lib/suretax/api/tax_amount_spec.rb +37 -0
  41. data/spec/lib/suretax/api/tax_spec.rb +59 -0
  42. data/spec/lib/suretax/configuration_spec.rb +97 -0
  43. data/spec/lib/suretax/connection_spec.rb +77 -0
  44. data/spec/lib/suretax/response_spec.rb +136 -0
  45. data/spec/spec_helper.rb +45 -0
  46. data/spec/support/cancellation_helper.rb +31 -0
  47. data/spec/support/connection_shared_examples.rb +37 -0
  48. data/spec/support/request_helper.rb +309 -0
  49. data/spec/support/suretax_helper.rb +27 -0
  50. data/spec/support/validations_shared_examples.rb +28 -0
  51. data/suretax.gemspec +33 -0
  52. metadata +281 -0
@@ -0,0 +1,84 @@
1
+ module Suretax
2
+ module Api
3
+
4
+ class RequestItem
5
+
6
+ include Suretax::Concerns::Validatable
7
+
8
+ attr_accessor :bill_to_number,
9
+ :customer_number,
10
+ :invoice_number,
11
+ :line_number,
12
+ :orig_number,
13
+ :p_to_p_plus_four,
14
+ :p_to_p_zipcode,
15
+ :plus_four,
16
+ :regulatory_code,
17
+ :revenue,
18
+ :sales_type_code,
19
+ :seconds,
20
+ :tax_included_code,
21
+ :tax_situs_rule,
22
+ :term_number,
23
+ :trans_date,
24
+ :trans_type_code,
25
+ :unit_type,
26
+ :units,
27
+ :zipcode,
28
+ :tax_exemption_codes
29
+
30
+ validate :bill_to_number,
31
+ :customer_number,
32
+ :invoice_number,
33
+ :line_number,
34
+ :orig_number,
35
+ :regulatory_code,
36
+ :sales_type_code,
37
+ :tax_situs_rule,
38
+ :term_number,
39
+ :trans_type_code,
40
+ :tax_exemption_codes
41
+
42
+ def initialize(args = {})
43
+ args.each_pair do |key,value|
44
+ self.send("#{key.to_s}=",value.to_s)
45
+ end
46
+
47
+ @tax_exemption_codes = []
48
+ unless args[:tax_exemption_codes].nil?
49
+ args[:tax_exemption_codes].each do |code|
50
+ @tax_exemption_codes << code.to_s
51
+ end
52
+ end
53
+
54
+ validate!
55
+ end
56
+
57
+ def params
58
+ {
59
+ "LineNumber" => line_number,
60
+ "InvoiceNumber" => invoice_number,
61
+ "CustomerNumber" => customer_number,
62
+ "OrigNumber" => orig_number || '',
63
+ "TermNumber" => term_number || '',
64
+ "BillToNumber" => bill_to_number || '',
65
+ "Zipcode" => zipcode,
66
+ "Plus4" => plus_four,
67
+ "P2PZipcode" => p_to_p_zipcode || '',
68
+ "P2PPlus4" => p_to_p_plus_four || '',
69
+ "TransDate" => trans_date || Date.today.strftime('%m-%d-%Y'),
70
+ "Revenue" => revenue.to_f,
71
+ "Units" => units.to_i,
72
+ "UnitType" => unit_type || '00',
73
+ "Seconds" => seconds.to_i,
74
+ "TaxIncludedCode" => tax_included_code,
75
+ "TaxSitusRule" => tax_situs_rule,
76
+ "TransTypeCode" => trans_type_code,
77
+ "SalesTypeCode" => sales_type_code,
78
+ "RegulatoryCode" => regulatory_code,
79
+ "TaxExemptionCodeList" => tax_exemption_codes
80
+ }
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,53 @@
1
+ require File.join( File.dirname(__FILE__), 'item_message.rb' )
2
+
3
+ module Suretax
4
+ module Api
5
+ class Response
6
+ attr_reader :status, :message, :total_tax, :groups, :transaction,
7
+ :client_tracking, :item_messages, :body
8
+
9
+ def initialize(response_body)
10
+ @body = JSON.generate(response_body)
11
+ @status = response_body.fetch('ResponseCode')
12
+ @transaction = response_body.fetch('TransId').to_s
13
+ @message = response_body.fetch('HeaderMessage')
14
+ @success = response_body.fetch('Successful') == 'Y'
15
+ @client_tracking = response_body['ClientTracking'] || nil
16
+ @total_tax = Amount.new(response_body['TotalTax'])
17
+
18
+ build_groups(response_body)
19
+ build_item_messages(response_body)
20
+ end
21
+
22
+ def success?
23
+ @success
24
+ end
25
+
26
+ def item_errors?
27
+ @status == '9001'
28
+ end
29
+
30
+ private
31
+
32
+ def build_groups(response_body)
33
+ @groups = []
34
+ if response_body['GroupList'].respond_to?(:map)
35
+ @groups = response_body.fetch('GroupList').map do |group|
36
+ Group.new(group)
37
+ end
38
+ end
39
+ end
40
+
41
+ def build_item_messages(response_body)
42
+ @item_messages = if item_errors?
43
+ response_body.fetch('ItemMessages').map do |item_message|
44
+ ItemMessage.new(item_message)
45
+ end
46
+ else
47
+ []
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+
@@ -0,0 +1,25 @@
1
+ module Suretax
2
+ module Api
3
+ class Tax
4
+ attr_reader :code, :description, :amount, :revenue, :county, :city, :rate, :taxable
5
+
6
+ def initialize(response_params)
7
+ @code = response_params.fetch('TaxTypeCode')
8
+ @description = response_params.fetch('TaxTypeDesc')
9
+ @amount = set_amount(response_params.fetch('TaxAmount').to_f)
10
+ @revenue = response_params['Revenue']
11
+ @county = response_params['CountyName']
12
+ @city = response_params['CityName']
13
+ @rate = set_amount(response_params['TaxRate'])
14
+ @taxable = set_amount(response_params['PercentTaxable'])
15
+ end
16
+
17
+ private
18
+
19
+ def set_amount(value)
20
+ Amount.new(value) unless value.nil?
21
+ end
22
+
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,46 @@
1
+ require 'monetize'
2
+
3
+ module Suretax
4
+ module Api
5
+ class Amount
6
+ attr_reader :precision, :divisor
7
+
8
+ def initialize(amount, currency = 'US6')
9
+ @amount = Monetize.parse(amount, currency)
10
+ @precision = count_significant_decimal_places
11
+ @divisor = @amount.currency.subunit_to_unit
12
+ end
13
+
14
+ def to_f
15
+ @amount.to_f
16
+ end
17
+
18
+ def to_s
19
+ @amount.to_s
20
+ end
21
+
22
+ def to_i
23
+ @amount.cents
24
+ end
25
+
26
+ def cents
27
+ (("%.2f" % to_f).to_f * 100 ).to_i
28
+ end
29
+
30
+ def params
31
+ {
32
+ amount: to_i,
33
+ precision: precision,
34
+ divisor: divisor
35
+ }
36
+ end
37
+
38
+ private
39
+
40
+ def count_significant_decimal_places
41
+ @amount.currency.subunit_to_unit.to_s.scan(/0/).count
42
+ end
43
+
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,7 @@
1
+ module Suretax
2
+ module Concerns
3
+
4
+ require 'suretax/concerns/validatable'
5
+
6
+ end
7
+ end
@@ -0,0 +1,208 @@
1
+ module Suretax::Concerns
2
+
3
+ module Validatable
4
+
5
+ def self.included(base)
6
+ base.extend ClassMethods
7
+ base.send(:include, Validations)
8
+ end
9
+
10
+ def errors
11
+ @errors = Errors.new
12
+
13
+ self.class.validatable_attributes.each do |attribute_name|
14
+ value = self.send(attribute_name)
15
+ assertion = self.send("valid_#{attribute_name}?", value)
16
+
17
+ @errors[attribute_name] = Error.new(attribute_name, value) unless assertion
18
+ end
19
+
20
+ @errors
21
+ end
22
+ alias_method :validate!, :errors
23
+
24
+ class Error
25
+
26
+ attr_accessor :message, :value, :attribute
27
+
28
+ def initialize(attribute, value)
29
+ self.value = value
30
+ self.attribute = attribute
31
+ self.message = "Invalid #{attribute}: #{reason}"
32
+ end
33
+
34
+ private
35
+
36
+ def reason
37
+ nested_errors? ? format_nested_errors(value) : format_error(value)
38
+ end
39
+
40
+ def format_nested_errors(value)
41
+ value.map { |obj| obj.errors.messages.join(', ') }
42
+ end
43
+
44
+ def format_error(value)
45
+ if value.nil?
46
+ "nil"
47
+ elsif value.is_a?(String) && value.empty?
48
+ %Q{''}
49
+ else
50
+ value
51
+ end
52
+ end
53
+
54
+ def nested_errors?
55
+ value.respond_to?(:each) && value.all? { |obj| obj.respond_to?(:errors) }
56
+ end
57
+ end
58
+
59
+ class Errors < Hash
60
+
61
+ def messages
62
+ self.map { |key, value| value.message }
63
+ end
64
+ end
65
+
66
+ module ClassMethods
67
+
68
+ attr_writer :validatable_attributes
69
+
70
+ def validate(*attribute_names)
71
+ self.validatable_attributes = attribute_names
72
+ end
73
+
74
+ def validatable_attributes
75
+ @validatable_attributes ||= []
76
+ end
77
+ end
78
+
79
+ module Validations
80
+
81
+ def valid_data_year?(value)
82
+ return false if blank?(value)
83
+ value.length == 4 &&
84
+ (value.to_i <= 2050 && value.to_i >= 1990)
85
+ end
86
+
87
+ def valid_data_month?(value)
88
+ matches?(value,month_list_subexpression)
89
+ end
90
+
91
+ def valid_items?(items)
92
+ ! items.any? {|item| item.errors.any? }
93
+ end
94
+
95
+ def valid_list?(value)
96
+ value.respond_to?(:each)
97
+ end
98
+
99
+ def valid_client_number?(value)
100
+ return false if blank?(value)
101
+ numeric?(value) && value.length <= 10
102
+ end
103
+ alias_method :valid_customer_number?, :valid_client_number?
104
+
105
+ def valid_business_unit?(value)
106
+ blank?(value) || (value.length <= 20 && alphanumeric?(value))
107
+ end
108
+
109
+ def valid_validation_key?(value)
110
+ return false if blank?(value)
111
+ value.length <= 36
112
+ end
113
+
114
+ def valid_total_revenue?(value)
115
+ matches?(value, total_revenue_positive_subexpression) ||
116
+ matches?(value, total_revenue_negative_subexpression)
117
+ end
118
+
119
+ def valid_return_file_code?(value)
120
+ matches?(value.to_s,'[Q0]')
121
+ end
122
+
123
+ def valid_client_tracking?(value)
124
+ blank?(value) || value.length <= 100
125
+ end
126
+
127
+ def valid_response_group?(value)
128
+ Suretax::RESPONSE_GROUPS.values.include?(value)
129
+ end
130
+
131
+ def valid_response_type?(value)
132
+ matches?(value, '[DS][1-9]')
133
+ end
134
+
135
+ def valid_line_number?(value)
136
+ blank?(value) || (value.length <= 20 && numeric?(value))
137
+ end
138
+
139
+ def valid_invoice_number?(value)
140
+ blank?(value) || (value.length <= 20 && alphanumeric?(value))
141
+ end
142
+
143
+ def valid_tax_situs_rule?(value)
144
+ Suretax::TAX_SITUS_RULES.values.include?(value)
145
+ end
146
+
147
+ def valid_trans_type_code?(value)
148
+ !blank?(value) && Suretax::TRANSACTION_TYPE_CODES.keys.include?(value)
149
+ end
150
+
151
+ def valid_sales_type_code?(value)
152
+ match_value = "[#{Suretax::SALES_TYPE_CODES.values.join}]"
153
+ !blank?(value) && matches?(value, match_value)
154
+ end
155
+
156
+ def valid_regulatory_code?(value)
157
+ !blank?(value) && Suretax::REGULATORY_CODES.values.include?(value)
158
+ end
159
+
160
+ def valid_tax_exemption_code_list?(value)
161
+ valid_list?(value) && !blank?(value.first)
162
+ end
163
+ alias_method :valid_tax_exemption_codes?, :valid_tax_exemption_code_list?
164
+
165
+ def optional_north_american_phone_number?(value)
166
+ blank?(value) || north_american_phone_number?(value)
167
+ end
168
+ alias_method :valid_bill_to_number?, :optional_north_american_phone_number?
169
+ alias_method :valid_orig_number?, :optional_north_american_phone_number?
170
+ alias_method :valid_term_number?, :optional_north_american_phone_number?
171
+
172
+ private
173
+
174
+ def north_american_phone_number?(value)
175
+ !blank?(value) && (value.length == 10 && numeric?(value))
176
+ end
177
+
178
+ def total_revenue_negative_subexpression
179
+ '-\d{,8}(?:\.\d{,4})?'
180
+ end
181
+
182
+ def total_revenue_positive_subexpression
183
+ '\d{,9}(?:\.\d{,4})?'
184
+ end
185
+
186
+ def numeric?(value)
187
+ matches?(value,'\d+')
188
+ end
189
+
190
+ def alphanumeric?(value)
191
+ matches?(value,'[a-z0-9]+')
192
+ end
193
+
194
+ def blank?(value)
195
+ value.nil? || matches?(value,'\s*')
196
+ end
197
+
198
+ def matches?(value,subexpression)
199
+ /\A#{subexpression}\z/i === value
200
+ end
201
+
202
+ # Month numbers 1-12 with optional leading zero
203
+ def month_list_subexpression
204
+ "0?(?:" + (1..12).to_a.join('|') + ")"
205
+ end
206
+ end
207
+ end
208
+ end
@@ -0,0 +1,84 @@
1
+ require "singleton"
2
+
3
+ module Suretax
4
+ class Configuration
5
+ include Singleton
6
+
7
+ REQUEST_VERSIONS = [1, 3]
8
+ CANCEL_VERSIONS = [1]
9
+
10
+ attr_accessor :validation_key, :base_url, :client_number,
11
+ :request_version, :cancel_version, :logger
12
+
13
+ def initialize
14
+ register_currencies
15
+ @base_url = test_host
16
+ @request_version = REQUEST_VERSIONS.max
17
+ @cancel_version = CANCEL_VERSIONS.max
18
+ end
19
+
20
+ def test?
21
+ base_url == test_host
22
+ end
23
+
24
+ def request_version=(version_number)
25
+ version = version_number.to_i
26
+ if REQUEST_VERSIONS.include?(version.to_i)
27
+ @request_version = version
28
+ else
29
+ raise(ArgumentError, "version must be in #{REQUEST_VERSIONS.join(', ')}")
30
+ end
31
+ @request_path = nil
32
+ end
33
+
34
+ def cancel_version=(version_number)
35
+ version = version_number.to_i
36
+ if CANCEL_VERSIONS.include?(version)
37
+ @cancel_version = version
38
+ else
39
+ raise(ArgumentError, "version must be in #{CANCEL_VERSIONS.join(', ')}")
40
+ end
41
+ @cancel_path = nil
42
+ end
43
+
44
+ def request_path
45
+ @request_path ||=
46
+ '/Services/V%02d/SureTax.asmx/PostRequest' % request_version
47
+ end
48
+
49
+ def cancel_path
50
+ @cancel_path ||=
51
+ '/Services/V%02d/SureTax.asmx/CancelPostRequest' % cancel_version
52
+ end
53
+
54
+ private
55
+
56
+ def test_host
57
+ 'https://testapi.taxrating.net'
58
+ end
59
+
60
+ def register_currencies
61
+ register_dollar_with_six_decimal_places
62
+ end
63
+
64
+ def register_dollar_with_six_decimal_places
65
+ Money::Currency.register(
66
+ {
67
+ priority: 1,
68
+ iso_code: "US6",
69
+ iso_numeric: "840",
70
+ name: "Dollar with six decimal places",
71
+ symbol: "$",
72
+ subunit: "Cent",
73
+ subunit_to_unit: 1000000,
74
+ symbol_first: true,
75
+ html_entity: "$",
76
+ decimal_mark: ".",
77
+ thousands_separator: ",",
78
+ symbolize_names: true
79
+ }
80
+ )
81
+ end
82
+
83
+ end
84
+ end