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.
@@ -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