valvat 1.1.5 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/.github/workflows/ruby.yml +2 -2
- data/.rubocop.yml +9 -3
- data/CHANGES.md +11 -2
- data/MIT-LICENSE +1 -1
- data/README.md +48 -33
- data/lib/valvat/checksum/es.rb +49 -16
- data/lib/valvat/error.rb +16 -16
- data/lib/valvat/local.rb +2 -49
- data/lib/valvat/lookup/base.rb +73 -0
- data/lib/valvat/lookup/hmrc.rb +86 -0
- data/lib/valvat/lookup/vies.rb +98 -0
- data/lib/valvat/lookup.rb +16 -5
- data/lib/valvat/utils.rb +0 -1
- data/lib/valvat/version.rb +1 -1
- data/lib/valvat.rb +50 -4
- data/spec/spec_helper.rb +2 -1
- data/spec/valvat/checksum/es_spec.rb +41 -1
- data/spec/valvat/checksum/gb_spec.rb +1 -0
- data/spec/valvat/lookup/hmrc_spec.rb +32 -0
- data/spec/valvat/lookup/vies_spec.rb +23 -0
- data/spec/valvat/lookup_spec.rb +259 -71
- data/valvat.gemspec +2 -2
- data.tar.gz.sig +0 -0
- metadata +22 -24
- metadata.gz.sig +0 -0
- data/lib/valvat/lookup/fault.rb +0 -44
- data/lib/valvat/lookup/request.rb +0 -57
- data/lib/valvat/lookup/response.rb +0 -37
- data/spec/valvat/lookup/fault_spec.rb +0 -34
- data/spec/valvat/lookup/request_spec.rb +0 -32
- data/spec/valvat/lookup/response_spec.rb +0 -29
@@ -0,0 +1,98 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base'
|
4
|
+
require 'net/http'
|
5
|
+
require 'erb'
|
6
|
+
require 'rexml'
|
7
|
+
|
8
|
+
class Valvat
|
9
|
+
class Lookup
|
10
|
+
class VIES < Base
|
11
|
+
ENDPOINT_URI = URI('https://ec.europa.eu/taxation_customs/vies/services/checkVatService').freeze
|
12
|
+
HEADERS = {
|
13
|
+
'Accept' => 'text/xml;charset=UTF-8',
|
14
|
+
'Content-Type' => 'text/xml;charset=UTF-8',
|
15
|
+
'SOAPAction' => ''
|
16
|
+
}.freeze
|
17
|
+
|
18
|
+
BODY = <<-XML.gsub(/^\s+/, '')
|
19
|
+
<soapenv:Envelope
|
20
|
+
xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
|
21
|
+
xmlns:urn="urn:ec.europa.eu:taxud:vies:services:checkVat:types">
|
22
|
+
<soapenv:Header/>
|
23
|
+
<soapenv:Body>
|
24
|
+
<urn:checkVat<%= 'Approx' if @requester %>>
|
25
|
+
<urn:countryCode><%= @vat.vat_country_code %></urn:countryCode>
|
26
|
+
<urn:vatNumber><%= @vat.to_s_wo_country %></urn:vatNumber>
|
27
|
+
<% if @requester %>
|
28
|
+
<urn:requesterCountryCode><%= @vat.vat_country_code %></urn:requesterCountryCode>
|
29
|
+
<urn:requesterVatNumber><%= @vat.to_s_wo_country %></urn:requesterVatNumber>
|
30
|
+
<% end %>
|
31
|
+
</urn:checkVat<%= 'Approx' if @requester %>>
|
32
|
+
</soapenv:Body>
|
33
|
+
</soapenv:Envelope>
|
34
|
+
XML
|
35
|
+
BODY_TEMPLATE = ERB.new(BODY).freeze
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def endpoint_uri
|
40
|
+
ENDPOINT_URI
|
41
|
+
end
|
42
|
+
|
43
|
+
def build_request(uri)
|
44
|
+
request = Net::HTTP::Post.new(uri.request_uri, HEADERS)
|
45
|
+
request.body = BODY_TEMPLATE.result(binding)
|
46
|
+
request
|
47
|
+
end
|
48
|
+
|
49
|
+
def parse(body)
|
50
|
+
doc = REXML::Document.new(body)
|
51
|
+
elements = doc.get_elements('/env:Envelope/env:Body').first.first
|
52
|
+
convert_values(elements.each_with_object({}) do |el, hash|
|
53
|
+
hash[convert_key(el.name)] = convert_value(el.text)
|
54
|
+
end)
|
55
|
+
end
|
56
|
+
|
57
|
+
def convert_key(key)
|
58
|
+
key.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
59
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
60
|
+
.tr('-', '_')
|
61
|
+
.sub(/\Atrader_/, '')
|
62
|
+
.downcase.to_sym
|
63
|
+
end
|
64
|
+
|
65
|
+
def convert_value(value)
|
66
|
+
value == '---' ? nil : value
|
67
|
+
end
|
68
|
+
|
69
|
+
def convert_values(hash)
|
70
|
+
return build_fault(hash) if hash[:faultstring]
|
71
|
+
|
72
|
+
hash[:valid] = hash[:valid] == 'true' || (hash[:valid] == 'false' ? false : nil) if hash.key?(:valid)
|
73
|
+
hash[:request_date] = Date.parse(hash[:request_date]) if hash.key?(:request_date)
|
74
|
+
hash
|
75
|
+
end
|
76
|
+
|
77
|
+
FAULTS = {
|
78
|
+
'SERVICE_UNAVAILABLE' => ServiceUnavailable,
|
79
|
+
'MS_UNAVAILABLE' => MemberStateUnavailable,
|
80
|
+
'INVALID_REQUESTER_INFO' => InvalidRequester,
|
81
|
+
'TIMEOUT' => Timeout,
|
82
|
+
'VAT_BLOCKED' => BlockedError,
|
83
|
+
'IP_BLOCKED' => BlockedError,
|
84
|
+
'GLOBAL_MAX_CONCURRENT_REQ' => RateLimitError,
|
85
|
+
'GLOBAL_MAX_CONCURRENT_REQ_TIME' => RateLimitError,
|
86
|
+
'MS_MAX_CONCURRENT_REQ' => RateLimitError,
|
87
|
+
'MS_MAX_CONCURRENT_REQ_TIME' => RateLimitError
|
88
|
+
}.freeze
|
89
|
+
|
90
|
+
def build_fault(hash)
|
91
|
+
fault = hash[:faultstring]
|
92
|
+
return hash.merge({ valid: false }) if fault == 'INVALID_INPUT'
|
93
|
+
|
94
|
+
hash.merge({ error: (FAULTS[fault] || UnknownLookupError).new(fault, self.class) })
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
data/lib/valvat/lookup.rb
CHANGED
@@ -1,5 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative 'lookup/vies'
|
4
|
+
require_relative 'lookup/hmrc'
|
5
|
+
|
3
6
|
class Valvat
|
4
7
|
class Lookup
|
5
8
|
def initialize(vat, options = {})
|
@@ -10,9 +13,9 @@ class Valvat
|
|
10
13
|
|
11
14
|
def validate
|
12
15
|
return false if !@options[:skip_local_validation] && !@vat.valid?
|
13
|
-
return
|
16
|
+
return handle_error(response[:error]) if response[:error]
|
14
17
|
|
15
|
-
response[:valid] && show_details? ? response
|
18
|
+
response[:valid] && show_details? ? response : response[:valid]
|
16
19
|
end
|
17
20
|
|
18
21
|
class << self
|
@@ -28,15 +31,23 @@ class Valvat
|
|
28
31
|
end
|
29
32
|
|
30
33
|
def response
|
31
|
-
@response ||=
|
34
|
+
@response ||= webservice.new(@vat, @options).perform
|
35
|
+
end
|
36
|
+
|
37
|
+
def webservice
|
38
|
+
case @vat.vat_country_code
|
39
|
+
when 'GB' then HMRC
|
40
|
+
else
|
41
|
+
VIES
|
42
|
+
end
|
32
43
|
end
|
33
44
|
|
34
45
|
def show_details?
|
35
46
|
@options[:requester] || @options[:detail]
|
36
47
|
end
|
37
48
|
|
38
|
-
def
|
39
|
-
if error.is_a?(
|
49
|
+
def handle_error(error)
|
50
|
+
if error.is_a?(MaintenanceError)
|
40
51
|
raise error if @options[:raise_error]
|
41
52
|
else
|
42
53
|
raise error unless @options[:raise_error] == false
|
data/lib/valvat/utils.rb
CHANGED
@@ -6,7 +6,6 @@ class Valvat
|
|
6
6
|
module Utils
|
7
7
|
EU_MEMBER_STATES = %w[AT BE BG CY CZ DE DK EE ES FI FR GR HR HU IE IT LT LU LV MT NL PL PT RO SE SI SK].freeze
|
8
8
|
SUPPORTED_STATES = EU_MEMBER_STATES + %w[GB]
|
9
|
-
EU_COUNTRIES = EU_MEMBER_STATES # TODO: Remove constant
|
10
9
|
COUNTRY_PATTERN = /\A([A-Z]{2})(.+)\Z/.freeze
|
11
10
|
NORMALIZE_PATTERN = /([[:punct:][:cntrl:]]|[[:space:]])+/.freeze
|
12
11
|
CONVERT_VAT_TO_ISO_COUNTRY = { 'EL' => 'GR', 'XI' => 'GB' }.freeze
|
data/lib/valvat/version.rb
CHANGED
data/lib/valvat.rb
CHANGED
@@ -1,16 +1,62 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'valvat/error'
|
4
|
-
require 'valvat/
|
4
|
+
require 'valvat/utils'
|
5
|
+
require 'valvat/syntax'
|
6
|
+
require 'valvat/checksum'
|
7
|
+
require 'valvat/version'
|
5
8
|
require 'valvat/lookup'
|
6
|
-
require 'valvat/lookup/request'
|
7
|
-
require 'valvat/lookup/response'
|
8
|
-
require 'valvat/lookup/fault'
|
9
9
|
require 'active_model/validations/valvat_validator' if defined?(ActiveModel)
|
10
10
|
|
11
11
|
class Valvat
|
12
|
+
def initialize(raw)
|
13
|
+
@raw = Valvat::Utils.normalize(raw || '')
|
14
|
+
@vat_country_code, @to_s_wo_country = to_a
|
15
|
+
end
|
16
|
+
|
17
|
+
attr_reader :raw, :vat_country_code, :to_s_wo_country
|
18
|
+
|
19
|
+
def blank?
|
20
|
+
raw.nil? || raw.strip == ''
|
21
|
+
end
|
22
|
+
|
23
|
+
def valid?
|
24
|
+
Valvat::Syntax.validate(self)
|
25
|
+
end
|
26
|
+
|
27
|
+
def valid_checksum?
|
28
|
+
Valvat::Checksum.validate(self)
|
29
|
+
end
|
30
|
+
|
12
31
|
def exists?(options = {})
|
13
32
|
Valvat::Lookup.validate(self, options)
|
14
33
|
end
|
15
34
|
alias exist? exists?
|
35
|
+
|
36
|
+
def iso_country_code
|
37
|
+
Valvat::Utils.vat_country_to_iso_country(vat_country_code)
|
38
|
+
end
|
39
|
+
|
40
|
+
# TODO: Remove method / not in use
|
41
|
+
def european?
|
42
|
+
puts 'DEPRECATED: #european? is deprecated. Instead access Valvat::Utils::EU_MEMBER_STATES directly.'
|
43
|
+
|
44
|
+
Valvat::Utils::EU_MEMBER_STATES.include?(iso_country_code)
|
45
|
+
end
|
46
|
+
|
47
|
+
def to_a
|
48
|
+
Valvat::Utils.split(raw)
|
49
|
+
end
|
50
|
+
|
51
|
+
def to_s
|
52
|
+
raw
|
53
|
+
end
|
54
|
+
|
55
|
+
def inspect
|
56
|
+
"#<Valvat #{[raw, iso_country_code].compact.join(' ')}>"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def Valvat(vat) # rubocop:disable Naming/MethodName
|
61
|
+
vat.is_a?(Valvat) ? vat : Valvat.new(vat)
|
16
62
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -8,12 +8,13 @@ rescue LoadError
|
|
8
8
|
end
|
9
9
|
|
10
10
|
require "#{File.dirname(__FILE__)}/../lib/valvat.rb"
|
11
|
+
require 'webmock/rspec'
|
12
|
+
WebMock.allow_net_connect!
|
11
13
|
|
12
14
|
RSpec.configure do |config|
|
13
15
|
config.mock_with :rspec
|
14
16
|
config.filter_run focus: true
|
15
17
|
config.run_all_when_everything_filtered = true
|
16
|
-
config.backtrace_exclusion_patterns = [%r{rspec/(core|expectations)}]
|
17
18
|
end
|
18
19
|
|
19
20
|
I18n.enforce_available_locales = false if defined?(I18n)
|
@@ -4,7 +4,7 @@ require 'spec_helper'
|
|
4
4
|
|
5
5
|
describe Valvat::Checksum::ES do
|
6
6
|
%w[ESA13585625 ESB83871236 ESE54507058 ES25139013J ESQ1518001A ESQ5018001G ESX4942978W ESX7676464F ESB10317980
|
7
|
-
ESY3860557K ESY2207765D].each do |valid_vat|
|
7
|
+
ESY3860557K ESY2207765D ES28350472M ES41961720Z ESM1171170X ESK0928769Y].each do |valid_vat|
|
8
8
|
it "returns true on valid VAT #{valid_vat}" do
|
9
9
|
expect(Valvat::Checksum.validate(valid_vat)).to be(true)
|
10
10
|
end
|
@@ -22,4 +22,44 @@ describe Valvat::Checksum::ES do
|
|
22
22
|
expect(Valvat::Checksum.validate(invalid_vat)).to be(false)
|
23
23
|
end
|
24
24
|
end
|
25
|
+
|
26
|
+
describe 'some CIF categories (NPQRSW) require control-digit to be a letter' do
|
27
|
+
invalid_vat = 'ESP65474207'
|
28
|
+
valid_vat = 'ESP6547420G'
|
29
|
+
|
30
|
+
it "returns false on invalid VAT #{invalid_vat}" do
|
31
|
+
expect(Valvat::Checksum.validate(invalid_vat)).to be(false)
|
32
|
+
end
|
33
|
+
|
34
|
+
it "returns true on valid VAT #{valid_vat}" do
|
35
|
+
expect(Valvat::Checksum.validate(valid_vat)).to be(true)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
describe 'some CIF categories (CDFGJNUV) allow both a numeric check digit and a letter' do
|
40
|
+
%w[ESC65474207 ESC6547420G].each do |valid_vat|
|
41
|
+
it "returns true on valid VAT #{valid_vat}" do
|
42
|
+
expect(Valvat::Checksum.validate(valid_vat)).to be(true)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe 'applies special rules to validation' do
|
48
|
+
describe 'special NIF categories (KLM) require CD to be a letter and first two digits '\
|
49
|
+
'to be between 01 and 56 (inclusive)' do
|
50
|
+
%w[ESK8201230M ESK0001230B].each do |invalid_vat|
|
51
|
+
it "returns false on invalid VAT #{invalid_vat}" do
|
52
|
+
expect(Valvat::Checksum.validate(invalid_vat)).to be(false)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
describe 'arbitrarily invalid VATs' do
|
58
|
+
%w[ESX0000000T ES00000001R ES00000000T ES99999999R].each do |invalid_vat|
|
59
|
+
it "returns false on invalid VAT #{invalid_vat}" do
|
60
|
+
expect(Valvat::Checksum.validate(invalid_vat)).to be(false)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
25
65
|
end
|
@@ -21,6 +21,7 @@ describe Valvat::Checksum::GB do
|
|
21
21
|
|
22
22
|
it 'is true for a new format valid vat' do
|
23
23
|
expect(Valvat::Checksum.validate('GB434031439')).to be true
|
24
|
+
expect(Valvat::Checksum.validate('GB727255821')).to be true
|
24
25
|
end
|
25
26
|
|
26
27
|
it 'is false for an old format VAT in forbidden group' do
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe Valvat::Lookup::HMRC do
|
6
|
+
before do
|
7
|
+
stub_const('Valvat::Lookup::HMRC::ENDPOINT_URL', 'https://test-api.service.hmrc.gov.uk/organisations/vat/check-vat-number/lookup')
|
8
|
+
end
|
9
|
+
|
10
|
+
it 'returns hash with valid: true on success' do
|
11
|
+
response = described_class.new('GB553557881', { uk: true }).perform
|
12
|
+
|
13
|
+
expect(response).to match({
|
14
|
+
valid: true,
|
15
|
+
address: "131B Barton Hamlet\nSW97 5CK\nGB",
|
16
|
+
country_code: 'GB',
|
17
|
+
vat_number: '553557881',
|
18
|
+
name: 'Credite Sberger Donal Inc.',
|
19
|
+
request_date: kind_of(Time)
|
20
|
+
})
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'returns hash with valid: false on invalid input' do
|
24
|
+
response = described_class.new('GB123456789', { uk: true }).perform
|
25
|
+
expect(response).to match({ valid: false })
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'returns hash with valid: false on valid input with :uk option not set' do
|
29
|
+
response = described_class.new('GB553557881', {}).perform
|
30
|
+
expect(response).to match({ valid: false })
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe Valvat::Lookup::VIES do
|
6
|
+
it 'returns hash with valid: true on success' do
|
7
|
+
response = described_class.new('IE6388047V', {}).perform
|
8
|
+
|
9
|
+
expect(response).to match({
|
10
|
+
valid: true,
|
11
|
+
address: '3RD FLOOR, GORDON HOUSE, BARROW STREET, DUBLIN 4',
|
12
|
+
country_code: 'IE',
|
13
|
+
vat_number: '6388047V',
|
14
|
+
name: 'GOOGLE IRELAND LIMITED',
|
15
|
+
request_date: kind_of(Date)
|
16
|
+
})
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'returns hash with valid: false on invalid input' do
|
20
|
+
response = described_class.new('XC123123', {}).perform
|
21
|
+
expect(response.to_hash).to match({ valid: false, faultstring: 'INVALID_INPUT', faultcode: 'env:Server' })
|
22
|
+
end
|
23
|
+
end
|