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.
- checksums.yaml +7 -0
- data/.gitignore +27 -0
- data/.travis.yml +14 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +74 -0
- data/Gemfile.travis +12 -0
- data/LICENSE.txt +22 -0
- data/NOTES.md +3 -0
- data/README.md +58 -0
- data/Rakefile +1 -0
- data/circle.yml +7 -0
- data/lib/suretax.rb +24 -0
- data/lib/suretax/api.rb +7 -0
- data/lib/suretax/api/cancel_request.rb +64 -0
- data/lib/suretax/api/group.rb +16 -0
- data/lib/suretax/api/item_message.rb +13 -0
- data/lib/suretax/api/request.rb +133 -0
- data/lib/suretax/api/request_item.rb +84 -0
- data/lib/suretax/api/response.rb +53 -0
- data/lib/suretax/api/tax.rb +25 -0
- data/lib/suretax/api/tax_amount.rb +46 -0
- data/lib/suretax/concerns.rb +7 -0
- data/lib/suretax/concerns/validatable.rb +208 -0
- data/lib/suretax/configuration.rb +84 -0
- data/lib/suretax/connection.rb +48 -0
- data/lib/suretax/constants.rb +7 -0
- data/lib/suretax/constants/regulatory_codes.rb +11 -0
- data/lib/suretax/constants/response_groups.rb +8 -0
- data/lib/suretax/constants/sales_type_codes.rb +8 -0
- data/lib/suretax/constants/tax_situs_codes.rb +12 -0
- data/lib/suretax/constants/transaction_type_codes.rb +505 -0
- data/lib/suretax/response.rb +70 -0
- data/lib/suretax/version.rb +3 -0
- data/spec/lib/suretax/api/group_spec.rb +50 -0
- data/spec/lib/suretax/api/request_item_spec.rb +54 -0
- data/spec/lib/suretax/api/request_item_validations_spec.rb +237 -0
- data/spec/lib/suretax/api/request_spec.rb +197 -0
- data/spec/lib/suretax/api/request_validations_spec.rb +384 -0
- data/spec/lib/suretax/api/response_spec.rb +165 -0
- data/spec/lib/suretax/api/tax_amount_spec.rb +37 -0
- data/spec/lib/suretax/api/tax_spec.rb +59 -0
- data/spec/lib/suretax/configuration_spec.rb +97 -0
- data/spec/lib/suretax/connection_spec.rb +77 -0
- data/spec/lib/suretax/response_spec.rb +136 -0
- data/spec/spec_helper.rb +45 -0
- data/spec/support/cancellation_helper.rb +31 -0
- data/spec/support/connection_shared_examples.rb +37 -0
- data/spec/support/request_helper.rb +309 -0
- data/spec/support/suretax_helper.rb +27 -0
- data/spec/support/validations_shared_examples.rb +28 -0
- data/suretax.gemspec +33 -0
- 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,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
|