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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6f6e189312895736dcfbb848edb51298272702f0a051133ce1fa10ff0ca332e9
4
- data.tar.gz: 772a816ac59d52a91428f94f998be5f42624a02c25698afea20c27789cb48071
3
+ metadata.gz: d0918fb1141de821b11aff7fb2d11da6ef7c75b2870e3b90a1373cfd26dc5fbe
4
+ data.tar.gz: f7b250f54690746d5339f6209625238461b7dd7edcc4d58adad5e095660186be
5
5
  SHA512:
6
- metadata.gz: 0d5827fce7a70c52def8b0869bca24fa7688363925ebc3e5c74f52f2f15a7b1d51277515f8a95418cb31ea1efdfc9b3357f5d00c7580a5a54a6920e22d83c815
7
- data.tar.gz: 7a5f8f4f1a5dac64cf8c449ab06a20751c73c8b86a0aa7b7eecc2f452b797435de303016886bc87bac6a8add7b5917e08da6441ded34a859ac90f6e897117d31
6
+ metadata.gz: f57c86ea54e7539887b3f8674035bca535379c5fcc1b532e4a995f0d67c3435f34e5195487db2ecabbbe6c3a0d1846ae70b232ce18ab9d2436dadd2bb7ef6864
7
+ data.tar.gz: d05a4f28813638f846d0e507c60e7f4f731d843914448c85b68ad3b4011f61784bb8dd15a501fd5bfdbd2c21c79ba7aa2692f47bece6701444dbf09bb77276a3
data/.rubocop.yml CHANGED
@@ -16,3 +16,6 @@ Style/RedundantRegexpCharacterClass:
16
16
 
17
17
  Style/CaseLikeIf:
18
18
  Enabled: false
19
+
20
+ Naming/PredicateMethod:
21
+ Enabled: false
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)
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'rexml/document'
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://europa.eu/youreurope/business/taxation/vat' \
16
- '/vat-rules-rates/index_en.htm'
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
- begin
30
- data = resp.scan(%r{\<tbody\>(.*)\<\/tbody\>}m).first.first.strip
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
- end
43
- # rubocop:enable Metrics/MethodLength
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
@@ -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 'rexml/document'
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
- WSDL = 'http://ec.europa.eu/taxation_customs/vies/' \
15
- 'services/checkVatService'
14
+ API_URL = 'https://ec.europa.eu/taxation_customs/vies/rest-api/check-vat-number'
16
15
  HEADERS = {
17
- 'Content-Type' => 'text/xml;charset=UTF-8',
18
- 'SOAPAction' => ''
16
+ 'Content-Type' => 'application/json',
17
+ 'Accept' => 'application/json'
19
18
  }.freeze
20
19
 
21
- BODY = <<-XML
22
- <soapenv:Envelope
23
- xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
24
- xmlns:urn="urn:ec.europa.eu:taxud:vies:services:checkVat:types">
25
- <soapenv:Header/>
26
- <soapenv:Body>
27
- <urn:checkVat>
28
- <urn:countryCode>{COUNTRY_CODE}</urn:countryCode>
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 :failed if number.size < 4
30
+ return :invalid_input if number.size < 4
37
31
 
38
32
  response = send_request(number[0..1], number[2..-1])
39
- return :failed unless response.is_a? Net::HTTPSuccess
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 Net::HTTPServerError
48
- :server_error
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 = response_xml(response)
51
+ body = JSON.parse(response.body)
53
52
  {
54
- valid: extract_data(body, 3) == 'true',
55
- country_code: extract_data(body, 0),
56
- vat_number: extract_data(body, 1),
57
- request_date: convert_date(extract_data(body, 2)),
58
- name: extract_data(body, 4),
59
- address: extract_data(body, 5)
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(WSDL)
87
-
88
- body = BODY.dup.gsub('{COUNTRY_CODE}', country_code)
89
- body = body.gsub('{NUMBER}', number)
90
-
91
- # Create the HTTP objects
92
- http = Net::HTTP.new(uri.host, uri.port)
93
- request = Net::HTTP::Post.new(uri.request_uri, HEADERS)
94
- request.body = body
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
@@ -2,5 +2,5 @@
2
2
 
3
3
  # Europe version
4
4
  module Europe
5
- VERSION = '0.0.26'
5
+ VERSION = '0.0.27'
6
6
  end
@@ -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 :failed, Europe::Vat.validate('6')
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 %i[timeout failed].include?(validate_correct_vat)
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 %i[timeout failed].include?(validate_correct_vat)
56
+ unless validate_correct_vat.is_a?(Symbol)
38
57
  end
39
58
 
40
- def test_failed_request_to_soap_service
59
+ def test_timeout
41
60
  WebMock.enable!
42
- stub_request(:any, 'http://ec.europa.eu/taxation_customs/vies/services/checkVatService').to_timeout
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
- stub_request(:get, 'http://ec.europa.eu/taxation_customs/vies/services/checkVatService')
46
- .with(headers: { 'Accept' => '*/*', 'User-Agent' => 'Ruby' })
47
- .to_return(status: 421, body: '')
48
- Europe::Vat.validate('DE115235681')
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.26
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: 2025-05-15 00:00:00.000000000 Z
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.23
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.