suretax 0.1.0

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