valvat 1.1.5 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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 handle_vies_error(response[:error]) if response[:error]
16
+ return handle_error(response[:error]) if response[:error]
14
17
 
15
- response[:valid] && show_details? ? response.to_hash : response[:valid]
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 ||= Request.new(@vat, @options).perform
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 handle_vies_error(error)
39
- if error.is_a?(ViesMaintenanceError)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Valvat
4
- VERSION = '1.1.5'
4
+ VERSION = '1.2.0'
5
5
  end
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/local'
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