valvat 1.1.5 → 1.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+ require 'date'
5
+ require 'net/http'
6
+ require 'erb'
7
+ require 'rexml'
8
+
9
+ class Valvat
10
+ class Lookup
11
+ class VIES < Base
12
+ ENDPOINT_URI = URI('https://ec.europa.eu/taxation_customs/vies/services/checkVatService').freeze
13
+ HEADERS = {
14
+ 'Accept' => 'text/xml;charset=UTF-8',
15
+ 'Content-Type' => 'text/xml;charset=UTF-8',
16
+ 'SOAPAction' => ''
17
+ }.freeze
18
+
19
+ BODY = <<-XML.gsub(/^\s+/, '')
20
+ <soapenv:Envelope
21
+ xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
22
+ xmlns:urn="urn:ec.europa.eu:taxud:vies:services:checkVat:types">
23
+ <soapenv:Header/>
24
+ <soapenv:Body>
25
+ <urn:checkVat<%= 'Approx' if @requester %>>
26
+ <urn:countryCode><%= @vat.vat_country_code %></urn:countryCode>
27
+ <urn:vatNumber><%= @vat.to_s_wo_country %></urn:vatNumber>
28
+ <% if @requester %>
29
+ <urn:requesterCountryCode><%= @vat.vat_country_code %></urn:requesterCountryCode>
30
+ <urn:requesterVatNumber><%= @vat.to_s_wo_country %></urn:requesterVatNumber>
31
+ <% end %>
32
+ </urn:checkVat<%= 'Approx' if @requester %>>
33
+ </soapenv:Body>
34
+ </soapenv:Envelope>
35
+ XML
36
+ BODY_TEMPLATE = ERB.new(BODY).freeze
37
+
38
+ private
39
+
40
+ def endpoint_uri
41
+ ENDPOINT_URI
42
+ end
43
+
44
+ def build_request(uri)
45
+ request = Net::HTTP::Post.new(uri.request_uri, HEADERS)
46
+ request.body = BODY_TEMPLATE.result(binding)
47
+ request
48
+ end
49
+
50
+ def parse(body)
51
+ doc = REXML::Document.new(body)
52
+ elements = doc.get_elements('/env:Envelope/env:Body').first.first
53
+ convert_values(elements.each_with_object({}) do |el, hash|
54
+ hash[convert_key(el.name)] = convert_value(el.text)
55
+ end)
56
+ end
57
+
58
+ def convert_key(key)
59
+ key.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
60
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
61
+ .tr('-', '_')
62
+ .sub(/\Atrader_/, '')
63
+ .downcase.to_sym
64
+ end
65
+
66
+ def convert_value(value)
67
+ value == '---' ? nil : value
68
+ end
69
+
70
+ def convert_values(hash)
71
+ return build_fault(hash) if hash[:faultstring]
72
+
73
+ hash[:valid] = hash[:valid] == 'true' || (hash[:valid] == 'false' ? false : nil) if hash.key?(:valid)
74
+ hash[:request_date] = Date.parse(hash[:request_date]) if hash.key?(:request_date)
75
+ hash
76
+ end
77
+
78
+ FAULTS = {
79
+ 'SERVICE_UNAVAILABLE' => ServiceUnavailable,
80
+ 'MS_UNAVAILABLE' => MemberStateUnavailable,
81
+ 'INVALID_REQUESTER_INFO' => InvalidRequester,
82
+ 'TIMEOUT' => Timeout,
83
+ 'VAT_BLOCKED' => BlockedError,
84
+ 'IP_BLOCKED' => BlockedError,
85
+ 'GLOBAL_MAX_CONCURRENT_REQ' => RateLimitError,
86
+ 'GLOBAL_MAX_CONCURRENT_REQ_TIME' => RateLimitError,
87
+ 'MS_MAX_CONCURRENT_REQ' => RateLimitError,
88
+ 'MS_MAX_CONCURRENT_REQ_TIME' => RateLimitError
89
+ }.freeze
90
+
91
+ def build_fault(hash)
92
+ fault = hash[:faultstring]
93
+ return hash.merge({ valid: false }) if fault == 'INVALID_INPUT'
94
+
95
+ hash.merge({ error: (FAULTS[fault] || UnknownLookupError).new(fault, self.class) })
96
+ end
97
+ end
98
+ end
99
+ 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.1'
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