valvat 1.1.4 → 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 +3 -3
- data/.rubocop.yml +11 -2
- data/CHANGES.md +19 -2
- data/MIT-LICENSE +1 -1
- data/README.md +151 -80
- data/gemfiles/activemodel-6 +3 -0
- data/gemfiles/activemodel-7 +3 -0
- data/gemfiles/standalone +3 -0
- data/lib/active_model/validations/valvat_validator.rb +1 -1
- data/lib/valvat/checksum/es.rb +49 -16
- data/lib/valvat/checksum/fr.rb +1 -1
- data/lib/valvat/checksum/hr.rb +1 -1
- data/lib/valvat/checksum/hu.rb +1 -1
- 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 +48 -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 +272 -80
- data/spec/valvat/utils_spec.rb +1 -1
- data/spec/valvat_spec.rb +12 -12
- data/valvat.gemspec +3 -2
- data.tar.gz.sig +0 -0
- metadata +24 -25
- metadata.gz.sig +0 -0
- data/lib/valvat/lookup/fault.rb +0 -38
- 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
data/lib/valvat/error.rb
CHANGED
@@ -3,32 +3,32 @@
|
|
3
3
|
class Valvat
|
4
4
|
Error = Class.new(RuntimeError)
|
5
5
|
|
6
|
-
class
|
7
|
-
def initialize(
|
8
|
-
@
|
9
|
-
@
|
10
|
-
super(
|
6
|
+
class LookupError < Error
|
7
|
+
def initialize(message, kind)
|
8
|
+
@message = message.to_s
|
9
|
+
@kind = kind.is_a?(Class) ? kind.name.split('::').last : kind.to_s
|
10
|
+
super(@message)
|
11
11
|
end
|
12
12
|
|
13
13
|
def to_s
|
14
|
-
"The
|
14
|
+
"The #{@kind} web service returned the error: #{@message}"
|
15
15
|
end
|
16
16
|
|
17
17
|
def eql?(other)
|
18
18
|
to_s.eql?(other.to_s)
|
19
19
|
end
|
20
20
|
end
|
21
|
-
|
21
|
+
MaintenanceError = Class.new(LookupError)
|
22
22
|
|
23
|
-
ServiceUnavailable = Class.new(
|
24
|
-
MemberStateUnavailable = Class.new(
|
23
|
+
ServiceUnavailable = Class.new(MaintenanceError)
|
24
|
+
MemberStateUnavailable = Class.new(MaintenanceError)
|
25
25
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
BlockedError = Class.new(ViesError)
|
31
|
-
RateLimitError = Class.new(ViesError)
|
26
|
+
Timeout = Class.new(LookupError)
|
27
|
+
InvalidRequester = Class.new(LookupError)
|
28
|
+
BlockedError = Class.new(LookupError)
|
29
|
+
RateLimitError = Class.new(LookupError)
|
32
30
|
|
33
|
-
|
31
|
+
UnknownLookupError = Class.new(LookupError)
|
32
|
+
|
33
|
+
HTTPError = Class.new(LookupError)
|
34
34
|
end
|
data/lib/valvat/local.rb
CHANGED
@@ -1,52 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
def initialize(raw)
|
5
|
-
@raw = Valvat::Utils.normalize(raw || '')
|
6
|
-
@vat_country_code, @to_s_wo_country = to_a
|
7
|
-
end
|
3
|
+
puts "DEPRECATED: Requiring 'valvat/local' is deprecated. Please require 'valvat' directly."
|
8
4
|
|
9
|
-
|
10
|
-
|
11
|
-
def blank?
|
12
|
-
raw.nil? || raw.strip == ''
|
13
|
-
end
|
14
|
-
|
15
|
-
def valid?
|
16
|
-
Valvat::Syntax.validate(self)
|
17
|
-
end
|
18
|
-
|
19
|
-
def valid_checksum?
|
20
|
-
Valvat::Checksum.validate(self)
|
21
|
-
end
|
22
|
-
|
23
|
-
def iso_country_code
|
24
|
-
Valvat::Utils.vat_country_to_iso_country(vat_country_code)
|
25
|
-
end
|
26
|
-
|
27
|
-
# TODO: Remove method / not in use
|
28
|
-
def european?
|
29
|
-
Valvat::Utils::EU_MEMBER_STATES.include?(iso_country_code)
|
30
|
-
end
|
31
|
-
|
32
|
-
def to_a
|
33
|
-
Valvat::Utils.split(raw)
|
34
|
-
end
|
35
|
-
|
36
|
-
def to_s
|
37
|
-
raw
|
38
|
-
end
|
39
|
-
|
40
|
-
def inspect
|
41
|
-
"#<Valvat #{[raw, iso_country_code].compact.join(' ')}>"
|
42
|
-
end
|
43
|
-
end
|
44
|
-
|
45
|
-
def Valvat(vat) # rubocop:disable Naming/MethodName
|
46
|
-
vat.is_a?(Valvat) ? vat : Valvat.new(vat)
|
47
|
-
end
|
48
|
-
|
49
|
-
require 'valvat/utils'
|
50
|
-
require 'valvat/syntax'
|
51
|
-
require 'valvat/checksum'
|
52
|
-
require 'valvat/version'
|
5
|
+
require_relative '../valvat'
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'net/http'
|
4
|
+
|
5
|
+
class Valvat
|
6
|
+
class Lookup
|
7
|
+
class Base
|
8
|
+
def initialize(vat, options = {})
|
9
|
+
@vat = Valvat(vat)
|
10
|
+
@options = options
|
11
|
+
@requester = @options[:requester] && Valvat(@options[:requester])
|
12
|
+
end
|
13
|
+
|
14
|
+
def perform
|
15
|
+
response = fetch(endpoint_uri)
|
16
|
+
|
17
|
+
case response
|
18
|
+
when Net::HTTPSuccess
|
19
|
+
parse(response.body)
|
20
|
+
else
|
21
|
+
{ error: Valvat::HTTPError.new(response.code, self.class) }
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def endpoint_uri
|
28
|
+
raise NotImplementedError
|
29
|
+
end
|
30
|
+
|
31
|
+
def build_request(uri)
|
32
|
+
raise NotImplementedError
|
33
|
+
end
|
34
|
+
|
35
|
+
def parse(body)
|
36
|
+
raise NotImplementedError
|
37
|
+
end
|
38
|
+
|
39
|
+
def fetch(uri, limit = 0)
|
40
|
+
response = send_request(uri)
|
41
|
+
|
42
|
+
if Net::HTTPRedirection == response && limit < 5
|
43
|
+
fetch(URI.parse(response['Location']), limit + 1)
|
44
|
+
else
|
45
|
+
response
|
46
|
+
end
|
47
|
+
rescue Errno::ECONNRESET
|
48
|
+
raise if limit > 5
|
49
|
+
|
50
|
+
fetch(uri, limit + 1)
|
51
|
+
end
|
52
|
+
|
53
|
+
def send_request(uri)
|
54
|
+
request = build_request(uri)
|
55
|
+
|
56
|
+
Net::HTTP.start(uri.host, uri.port, options_for(uri)) do |http|
|
57
|
+
http.request(request)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def options_for(uri)
|
62
|
+
options = if @options.key?(:savon)
|
63
|
+
puts 'DEPRECATED: The option :savon is deprecated. Use :http instead.'
|
64
|
+
@options[:savon]
|
65
|
+
else
|
66
|
+
@options[:http]
|
67
|
+
end || {}
|
68
|
+
|
69
|
+
options.merge({ use_ssl: URI::HTTPS === uri })
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base'
|
4
|
+
require 'net/http'
|
5
|
+
require 'json'
|
6
|
+
|
7
|
+
class Valvat
|
8
|
+
class Lookup
|
9
|
+
class HMRC < Base
|
10
|
+
ENDPOINT_URL = 'https://api.service.hmrc.gov.uk/organisations/vat/check-vat-number/lookup'
|
11
|
+
HEADERS = {
|
12
|
+
# https://developer.service.hmrc.gov.uk/api-documentation/docs/reference-guide#versioning
|
13
|
+
'Accept' => 'application/vnd.hmrc.1.0+json'
|
14
|
+
}.freeze
|
15
|
+
|
16
|
+
def perform
|
17
|
+
return { valid: false } unless @options[:uk] == true
|
18
|
+
|
19
|
+
parse(fetch(endpoint_uri).body)
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def endpoint_uri
|
25
|
+
endpoint = "/#{@vat.to_s_wo_country}"
|
26
|
+
endpoint += "/#{@requester.to_s_wo_country}" if @requester
|
27
|
+
URI.parse(ENDPOINT_URL + endpoint)
|
28
|
+
end
|
29
|
+
|
30
|
+
def build_request(uri)
|
31
|
+
Net::HTTP::Get.new(uri.request_uri, HEADERS)
|
32
|
+
end
|
33
|
+
|
34
|
+
def parse(body)
|
35
|
+
convert(JSON.parse(body))
|
36
|
+
end
|
37
|
+
|
38
|
+
# Return a similar format to VIES
|
39
|
+
# Main differences are:
|
40
|
+
# - request_date is a (more precise) Time instead of Date
|
41
|
+
# - address is newline separated instead of coma (also more precise)
|
42
|
+
def convert(raw)
|
43
|
+
return build_fault(raw) if raw.key?('code')
|
44
|
+
|
45
|
+
{
|
46
|
+
address: format_address(raw.dig('target', 'address')),
|
47
|
+
country_code: raw.dig('target', 'address', 'countryCode'),
|
48
|
+
name: raw.dig('target', 'name'),
|
49
|
+
vat_number: raw.dig('target', 'vatNumber'), valid: true
|
50
|
+
}.tap do |hash|
|
51
|
+
hash[:request_date] = Time.parse(raw['processingDate']) if raw.key?('processingDate')
|
52
|
+
hash[:request_identifier] = raw['consultationNumber'] if raw.key?('consultationNumber')
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Example raw address from the API:
|
57
|
+
# {
|
58
|
+
# "line1": "HM REVENUE AND CUSTOMS",
|
59
|
+
# "line2": "RUBY HOUSE",
|
60
|
+
# "line3": "8 RUBY PLACE",
|
61
|
+
# "line4": "ABERDEEN",
|
62
|
+
# "postcode": "AB10 1ZP",
|
63
|
+
# "countryCode": "GB"
|
64
|
+
# }
|
65
|
+
def format_address(address)
|
66
|
+
address&.values&.join("\n")
|
67
|
+
end
|
68
|
+
|
69
|
+
FAULTS = {
|
70
|
+
'MESSAGE_THROTTLED_OUT' => RateLimitError,
|
71
|
+
'SCHEDULED_MAINTENANCE' => ServiceUnavailable,
|
72
|
+
'SERVER_ERROR' => ServiceUnavailable,
|
73
|
+
'INVALID_REQUEST' => InvalidRequester,
|
74
|
+
'GATEWAY_TIMEOUT' => Timeout
|
75
|
+
}.freeze
|
76
|
+
|
77
|
+
def build_fault(raw)
|
78
|
+
fault = raw['code']
|
79
|
+
return { valid: false } if fault == 'NOT_FOUND'
|
80
|
+
|
81
|
+
exception = FAULTS[fault] || UnknownLookupError
|
82
|
+
{ error: exception.new("#{fault}#{raw['message'] ? " (#{raw['message']})" : ''}", self.class) }
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -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
|
@@ -15,4 +15,51 @@ describe Valvat::Checksum::ES do
|
|
15
15
|
expect(Valvat::Checksum.validate(invalid_vat)).to be(false)
|
16
16
|
end
|
17
17
|
end
|
18
|
+
|
19
|
+
describe 'if starts with [KLMXYZ\\d], is always a natural person' do
|
20
|
+
invalid_vat = 'ESX65474207'
|
21
|
+
it "returns false on invalid VAT #{invalid_vat}" do
|
22
|
+
expect(Valvat::Checksum.validate(invalid_vat)).to be(false)
|
23
|
+
end
|
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
|
18
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
|