valvat 1.1.5 → 1.2.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 +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
|