europe 0.0.26 → 0.0.27
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
- data/.rubocop.yml +3 -0
- data/CHANGELOG.md +5 -0
- data/lib/europe/vat/rates.rb +11 -26
- data/lib/europe/vat/vat.rb +42 -54
- data/lib/europe/version.rb +1 -1
- data/test/europe/vat/validation_test.rb +82 -10
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d0918fb1141de821b11aff7fb2d11da6ef7c75b2870e3b90a1373cfd26dc5fbe
|
|
4
|
+
data.tar.gz: f7b250f54690746d5339f6209625238461b7dd7edcc4d58adad5e095660186be
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f57c86ea54e7539887b3f8674035bca535379c5fcc1b532e4a995f0d67c3435f34e5195487db2ecabbbe6c3a0d1846ae70b232ce18ab9d2436dadd2bb7ef6864
|
|
7
|
+
data.tar.gz: d05a4f28813638f846d0e507c60e7f4f731d843914448c85b68ad3b4011f61784bb8dd15a501fd5bfdbd2c21c79ba7aa2692f47bece6701444dbf09bb77276a3
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
# Change Log
|
|
2
2
|
All notable changes to this project will be documented in this file.
|
|
3
3
|
This project adheres to [Semantic Versioning](http://semver.org/).
|
|
4
|
+
## 0.0.27
|
|
5
|
+
- Updated VIES VAT check service to their REST API instead of their SOAP API
|
|
6
|
+
- Improved error handling for the VAT check service
|
|
7
|
+
- Changed the endpoint to check for VAT rates to increase it's stability
|
|
8
|
+
- [Full Changelog](https://github.com/gem-shards/europe.rb/compare/v0.0.26...v0.0.27)
|
|
4
9
|
## 0.0.26
|
|
5
10
|
- Removed spaces by default for VAT number format validations, as per request from tim-vandecasteele
|
|
6
11
|
- [Full Changelog](https://github.com/gem-shards/europe.rb/compare/v0.0.25...v0.0.26)
|
data/lib/europe/vat/rates.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require '
|
|
3
|
+
require 'json'
|
|
4
4
|
|
|
5
5
|
module Europe
|
|
6
6
|
module Vat
|
|
@@ -12,8 +12,10 @@ module Europe
|
|
|
12
12
|
IT: 22.0, LT: 21.0, LU: 17.0, LV: 21.0, MT: 18.0, NL: 21.0, PL: 23.0, PT: 23.0,
|
|
13
13
|
RO: 19.0, SE: 25.0, SI: 22.0, SK: 23.0
|
|
14
14
|
}.freeze
|
|
15
|
-
RATES_URL = 'https://
|
|
16
|
-
'/
|
|
15
|
+
RATES_URL = 'https://raw.githubusercontent.com/benbucksch/eu-vat-rates' \
|
|
16
|
+
'/refs/heads/master/rates.json'
|
|
17
|
+
|
|
18
|
+
COUNTRY_CODE_MAP = { GR: :EL }.freeze
|
|
17
19
|
|
|
18
20
|
def self.retrieve
|
|
19
21
|
resp = fetch_rates
|
|
@@ -22,33 +24,16 @@ module Europe
|
|
|
22
24
|
extract_rates(resp)
|
|
23
25
|
end
|
|
24
26
|
|
|
25
|
-
# rubocop:disable Metrics/MethodLength
|
|
26
27
|
def self.extract_rates(resp)
|
|
28
|
+
data = JSON.parse(resp)
|
|
27
29
|
rates = {}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
rescue NoMethodError
|
|
32
|
-
return FALLBACK_RATES
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
xml = REXML::Document.new("<root>#{data}</root>")
|
|
36
|
-
xml.first.elements.each('tr') do |result|
|
|
37
|
-
next if result[3].nil?
|
|
38
|
-
|
|
39
|
-
rates = filter_rate(result, rates || {})
|
|
30
|
+
data['rates'].each do |code, info|
|
|
31
|
+
key = COUNTRY_CODE_MAP[code.to_sym] || code.to_sym
|
|
32
|
+
rates[key] = info['standard_rate'].to_f
|
|
40
33
|
end
|
|
41
34
|
rates
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
def self.filter_rate(result, rates)
|
|
46
|
-
return unless result[1].text.size == 2
|
|
47
|
-
|
|
48
|
-
country = result[1].text
|
|
49
|
-
rate = result[5].text
|
|
50
|
-
rates[country.to_sym] = rate.to_f if country && rate
|
|
51
|
-
rates
|
|
35
|
+
rescue JSON::ParserError
|
|
36
|
+
FALLBACK_RATES
|
|
52
37
|
end
|
|
53
38
|
|
|
54
39
|
def self.fetch_rates
|
data/lib/europe/vat/vat.rb
CHANGED
|
@@ -4,77 +4,67 @@ require 'europe/vat/rates'
|
|
|
4
4
|
require 'europe/vat/format'
|
|
5
5
|
require 'uri'
|
|
6
6
|
require 'net/http'
|
|
7
|
-
require '
|
|
7
|
+
require 'json'
|
|
8
8
|
require 'date'
|
|
9
9
|
|
|
10
10
|
# Europe Gem
|
|
11
11
|
module Europe
|
|
12
12
|
# VAT
|
|
13
13
|
module Vat
|
|
14
|
-
|
|
15
|
-
'services/checkVatService'
|
|
14
|
+
API_URL = 'https://ec.europa.eu/taxation_customs/vies/rest-api/check-vat-number'
|
|
16
15
|
HEADERS = {
|
|
17
|
-
'Content-Type' => '
|
|
18
|
-
'
|
|
16
|
+
'Content-Type' => 'application/json',
|
|
17
|
+
'Accept' => 'application/json'
|
|
19
18
|
}.freeze
|
|
20
19
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
<urn:vatNumber>{NUMBER}</urn:vatNumber>
|
|
30
|
-
</urn:checkVat>
|
|
31
|
-
</soapenv:Body>
|
|
32
|
-
</soapenv:Envelope>
|
|
33
|
-
XML
|
|
20
|
+
VIES_ERRORS = {
|
|
21
|
+
'INVALID_INPUT' => :invalid_input,
|
|
22
|
+
'GLOBAL_MAX_CONCURRENT_REQ' => :rate_limited,
|
|
23
|
+
'MS_MAX_CONCURRENT_REQ' => :rate_limited,
|
|
24
|
+
'SERVICE_UNAVAILABLE' => :service_unavailable,
|
|
25
|
+
'MS_UNAVAILABLE' => :ms_unavailable,
|
|
26
|
+
'TIMEOUT' => :timeout
|
|
27
|
+
}.freeze
|
|
34
28
|
|
|
35
29
|
def self.validate(number)
|
|
36
|
-
return :
|
|
30
|
+
return :invalid_input if number.size < 4
|
|
37
31
|
|
|
38
32
|
response = send_request(number[0..1], number[2..-1])
|
|
39
|
-
return
|
|
40
|
-
return :failed if response.body.include?('soap:Fault')
|
|
41
|
-
return :timeout if response.body.include?('TIMEOUT')
|
|
42
|
-
return :timeout if response.body.include?('MS_MAX_CONCURRENT_REQ')
|
|
33
|
+
return handle_error_response(response) unless response.is_a?(Net::HTTPSuccess)
|
|
43
34
|
|
|
44
35
|
setup_response(response)
|
|
45
|
-
rescue Net::OpenTimeout
|
|
36
|
+
rescue Net::OpenTimeout, Net::ReadTimeout
|
|
46
37
|
:timeout
|
|
47
|
-
rescue
|
|
48
|
-
:
|
|
38
|
+
rescue Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::ETIMEDOUT, SocketError, OpenSSL::SSL::SSLError
|
|
39
|
+
:service_unavailable
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def self.handle_error_response(response)
|
|
43
|
+
body = JSON.parse(response.body)
|
|
44
|
+
error_code = body.dig('errorWrappers', 0, 'error')
|
|
45
|
+
VIES_ERRORS[error_code] || :service_error
|
|
46
|
+
rescue JSON::ParserError
|
|
47
|
+
:service_error
|
|
49
48
|
end
|
|
50
49
|
|
|
51
50
|
def self.setup_response(response)
|
|
52
|
-
body =
|
|
51
|
+
body = JSON.parse(response.body)
|
|
53
52
|
{
|
|
54
|
-
valid:
|
|
55
|
-
country_code:
|
|
56
|
-
vat_number:
|
|
57
|
-
request_date: convert_date(
|
|
58
|
-
name:
|
|
59
|
-
address:
|
|
53
|
+
valid: body['valid'] == true,
|
|
54
|
+
country_code: body['countryCode'],
|
|
55
|
+
vat_number: body['vatNumber'],
|
|
56
|
+
request_date: convert_date(body['requestDate']),
|
|
57
|
+
name: body['name'],
|
|
58
|
+
address: body['address']
|
|
60
59
|
}
|
|
61
60
|
end
|
|
62
61
|
|
|
63
|
-
def self.response_xml(response)
|
|
64
|
-
xml = REXML::Document.new(response.body)
|
|
65
|
-
xml.elements.first.elements[2].elements[1]
|
|
66
|
-
end
|
|
67
|
-
|
|
68
62
|
def self.convert_date(date)
|
|
69
63
|
return unless date
|
|
70
64
|
|
|
71
65
|
Date.parse(date)
|
|
72
66
|
end
|
|
73
67
|
|
|
74
|
-
def self.extract_data(body, position)
|
|
75
|
-
body[position]&.text
|
|
76
|
-
end
|
|
77
|
-
|
|
78
68
|
def self.charge_vat?(origin_country, number)
|
|
79
69
|
return false if number.nil? || number.empty?
|
|
80
70
|
|
|
@@ -83,18 +73,16 @@ module Europe
|
|
|
83
73
|
end
|
|
84
74
|
|
|
85
75
|
def self.send_request(country_code, number)
|
|
86
|
-
uri = URI.parse(
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
# Send the request
|
|
97
|
-
http.request(request)
|
|
76
|
+
uri = URI.parse(API_URL)
|
|
77
|
+
|
|
78
|
+
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
|
|
79
|
+
request = Net::HTTP::Post.new(uri.request_uri, HEADERS)
|
|
80
|
+
request.body = JSON.generate(
|
|
81
|
+
countryCode: country_code,
|
|
82
|
+
vatNumber: number
|
|
83
|
+
)
|
|
84
|
+
http.request(request)
|
|
85
|
+
end
|
|
98
86
|
end
|
|
99
87
|
end
|
|
100
88
|
end
|
data/lib/europe/version.rb
CHANGED
|
@@ -4,6 +4,25 @@ require 'test_helper'
|
|
|
4
4
|
|
|
5
5
|
module Europe
|
|
6
6
|
module Vat
|
|
7
|
+
SERVICE_UNAVAILABLE_BODY = '{"actionSucceed":false,"errorWrappers":' \
|
|
8
|
+
'[{"error":"SERVICE_UNAVAILABLE","message":' \
|
|
9
|
+
'"An error was encountered either at the national ' \
|
|
10
|
+
'level or at the European Commission"}]}'
|
|
11
|
+
MS_UNAVAILABLE_BODY = '{"actionSucceed":false,"errorWrappers":' \
|
|
12
|
+
'[{"error":"MS_UNAVAILABLE","message":"The Member State service is unavailable"}]}'
|
|
13
|
+
GLOBAL_RATE_LIMITED_BODY = '{"actionSucceed":false,"errorWrappers":' \
|
|
14
|
+
'[{"error":"GLOBAL_MAX_CONCURRENT_REQ","message":' \
|
|
15
|
+
'"Your Request for VAT validation has not been processed; ' \
|
|
16
|
+
'your max. number of concurrent requests has been reached"}]}'
|
|
17
|
+
MS_RATE_LIMITED_BODY = '{"actionSucceed":false,"errorWrappers":' \
|
|
18
|
+
'[{"error":"MS_MAX_CONCURRENT_REQ","message":' \
|
|
19
|
+
'"Your Request for VAT validation has not been processed; ' \
|
|
20
|
+
'your max. number of concurrent requests for this Member State has been reached"}]}'
|
|
21
|
+
INVALID_INPUT_BODY = '{"actionSucceed":false,"errorWrappers":' \
|
|
22
|
+
'[{"error":"INVALID_INPUT","message":"The provided CountryCode is invalid"}]}'
|
|
23
|
+
VIES_TIMEOUT_BODY = '{"actionSucceed":false,"errorWrappers":' \
|
|
24
|
+
'[{"error":"TIMEOUT","message":"The Member State service could not be reached in time"}]}'
|
|
25
|
+
|
|
7
26
|
# ValidationTest
|
|
8
27
|
class ValidationTest < Minitest::Test
|
|
9
28
|
include Benchmark
|
|
@@ -13,7 +32,7 @@ module Europe
|
|
|
13
32
|
end
|
|
14
33
|
|
|
15
34
|
def test_validation_of_short_vat_number
|
|
16
|
-
assert_equal :
|
|
35
|
+
assert_equal :invalid_input, Europe::Vat.validate('6')
|
|
17
36
|
end
|
|
18
37
|
|
|
19
38
|
def test_validation_of_false_vat_number
|
|
@@ -29,23 +48,76 @@ module Europe
|
|
|
29
48
|
# PostNL
|
|
30
49
|
validate_correct_vat = Europe::Vat.validate('NL009291477B01')
|
|
31
50
|
assert validate_correct_vat[:valid] \
|
|
32
|
-
unless
|
|
51
|
+
unless validate_correct_vat.is_a?(Symbol)
|
|
33
52
|
|
|
34
53
|
# Volkswagen
|
|
35
54
|
validate_correct_vat = Europe::Vat.validate('DE115235681')
|
|
36
55
|
assert validate_correct_vat[:valid] \
|
|
37
|
-
unless
|
|
56
|
+
unless validate_correct_vat.is_a?(Symbol)
|
|
38
57
|
end
|
|
39
58
|
|
|
40
|
-
def
|
|
59
|
+
def test_timeout
|
|
41
60
|
WebMock.enable!
|
|
42
|
-
stub_request(:
|
|
43
|
-
Europe::Vat.validate('DE115235681')
|
|
61
|
+
stub_request(:post, Europe::Vat::API_URL).to_timeout
|
|
62
|
+
assert_equal :timeout, Europe::Vat.validate('DE115235681')
|
|
63
|
+
WebMock.disable!
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def test_connection_refused
|
|
67
|
+
WebMock.enable!
|
|
68
|
+
stub_request(:post, Europe::Vat::API_URL).to_raise(Errno::ECONNREFUSED)
|
|
69
|
+
assert_equal :service_unavailable, Europe::Vat.validate('DE115235681')
|
|
70
|
+
WebMock.disable!
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def test_server_error_without_body
|
|
74
|
+
WebMock.enable!
|
|
75
|
+
stub_request(:post, Europe::Vat::API_URL)
|
|
76
|
+
.to_return(status: 500, body: '')
|
|
77
|
+
assert_equal :service_error, Europe::Vat.validate('DE115235681')
|
|
78
|
+
WebMock.disable!
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def test_service_unavailable_error
|
|
82
|
+
assert_vies_error 500, SERVICE_UNAVAILABLE_BODY,
|
|
83
|
+
'DE115235681', :service_unavailable
|
|
84
|
+
end
|
|
44
85
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
86
|
+
def test_ms_unavailable_error
|
|
87
|
+
assert_vies_error 500, MS_UNAVAILABLE_BODY,
|
|
88
|
+
'DE115235681', :ms_unavailable
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def test_rate_limited_global
|
|
92
|
+
assert_vies_error 500, GLOBAL_RATE_LIMITED_BODY,
|
|
93
|
+
'DE115235681', :rate_limited
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def test_rate_limited_member_state
|
|
97
|
+
assert_vies_error 500, MS_RATE_LIMITED_BODY,
|
|
98
|
+
'DE115235681', :rate_limited
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def test_invalid_input_error
|
|
102
|
+
assert_vies_error 400, INVALID_INPUT_BODY,
|
|
103
|
+
'XX123456789', :invalid_input
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def test_vies_timeout_error
|
|
107
|
+
assert_vies_error 500, VIES_TIMEOUT_BODY,
|
|
108
|
+
'DE115235681', :timeout
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
private
|
|
112
|
+
|
|
113
|
+
def assert_vies_error(status, body, vat, expected)
|
|
114
|
+
WebMock.enable!
|
|
115
|
+
stub_request(:post, Europe::Vat::API_URL)
|
|
116
|
+
.to_return(
|
|
117
|
+
status: status, body: body,
|
|
118
|
+
headers: { 'Content-Type' => 'application/json' }
|
|
119
|
+
)
|
|
120
|
+
assert_equal expected, Europe::Vat.validate(vat)
|
|
49
121
|
WebMock.disable!
|
|
50
122
|
end
|
|
51
123
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: europe
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.27
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Gem shards
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2026-06-08 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: byebug
|
|
@@ -199,7 +199,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
199
199
|
- !ruby/object:Gem::Version
|
|
200
200
|
version: '0'
|
|
201
201
|
requirements: []
|
|
202
|
-
rubygems_version: 3.5.
|
|
202
|
+
rubygems_version: 3.5.9
|
|
203
203
|
signing_key:
|
|
204
204
|
specification_version: 4
|
|
205
205
|
summary: Europe is a gem for retrieving and validating EU government data.
|