valvat 1.4.4 → 2.0.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/lib/valvat/configuration.rb +15 -3
- data/lib/valvat/error.rb +2 -0
- data/lib/valvat/hmrc/access_token.rb +102 -0
- data/lib/valvat/lookup/base.rb +3 -2
- data/lib/valvat/lookup/hmrc.rb +20 -6
- data/lib/valvat/options.rb +15 -0
- data/lib/valvat/utils.rb +5 -0
- data/lib/valvat/version.rb +1 -1
- data.tar.gz.sig +0 -0
- metadata +5 -4
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 42c6f6cebfde595c408697bfd0fcad362e8978fb11b84e6c3f3e62d5942c8644
|
4
|
+
data.tar.gz: 10e1ca8684e582d0b94c87d2d559ce2fbaeb6977d888f717b3e64b7daa1246e2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 03b6d77c886adb21ee7c2b03cb07c11c44f5ec69ad504e390408c38a04e876476e8acaa87d64f6332a137c045bd1dbecfd9d0b2ddbf22111efd80caec58e161b
|
7
|
+
data.tar.gz: 6f0fc3387f1761f387de1c3ff3168018bc12872711f44369d2528033301e4e77396e69aaaa5f0229903079297048d8a4cd7d6defc3c204798364e42c53c506e3
|
checksums.yaml.gz.sig
CHANGED
Binary file
|
data/lib/valvat/configuration.rb
CHANGED
@@ -7,7 +7,7 @@ class Valvat
|
|
7
7
|
# Configuration options should be set by passing a hash:
|
8
8
|
#
|
9
9
|
# Valvat.configure(
|
10
|
-
# uk:
|
10
|
+
# uk: { client_id: '<client_id>', client_secret: '<client_secret>' }
|
11
11
|
# )
|
12
12
|
#
|
13
13
|
def self.configure(options)
|
@@ -42,7 +42,15 @@ class Valvat
|
|
42
42
|
|
43
43
|
# Use lookup via HMRC for VAT numbers from the UK
|
44
44
|
# if set to false lookup will always return false for UK VAT numbers
|
45
|
-
|
45
|
+
# HMRC options:
|
46
|
+
# :client_id and :client_secret API credentials for OAuth 2.0 Authentication
|
47
|
+
# :sandbox Use sandboxed instead of production API (defaults to false)
|
48
|
+
# See more details https://developer.service.hmrc.gov.uk/api-documentation/docs/development-practices
|
49
|
+
#
|
50
|
+
uk: false,
|
51
|
+
|
52
|
+
# Rate limit for Lookup and HMRC authentication requests
|
53
|
+
rate_limit: 5
|
46
54
|
}.freeze
|
47
55
|
|
48
56
|
def self.initialize
|
@@ -53,8 +61,12 @@ class Valvat
|
|
53
61
|
@data[key]
|
54
62
|
end
|
55
63
|
|
64
|
+
def dig(*keys)
|
65
|
+
@data.dig(*keys)
|
66
|
+
end
|
67
|
+
|
56
68
|
def configure(options)
|
57
|
-
@data = @data
|
69
|
+
@data = Utils.deep_merge(@data, Utils.deep_symbolize_keys(options))
|
58
70
|
end
|
59
71
|
|
60
72
|
def initialize
|
data/lib/valvat/error.rb
CHANGED
@@ -0,0 +1,102 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'net/http'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
class Valvat
|
7
|
+
module HMRC
|
8
|
+
class AccessToken
|
9
|
+
Error = Class.new(StandardError)
|
10
|
+
|
11
|
+
PRODUCTION_ENDPOINT_URL = 'https://api.service.hmrc.gov.uk/oauth/token'
|
12
|
+
SANDBOX_ENDPOINT_URL = 'https://test-api.service.hmrc.gov.uk/oauth/token'
|
13
|
+
SCOPE = 'read:vat'
|
14
|
+
GRANT_TYPE = 'client_credentials'
|
15
|
+
|
16
|
+
def initialize(options = {})
|
17
|
+
uk_options = options[:uk].is_a?(Hash) ? options[:uk] : {}
|
18
|
+
|
19
|
+
@client_id = uk_options[:client_id].to_s
|
20
|
+
@client_secret = uk_options[:client_secret].to_s
|
21
|
+
@rate_limit = options[:rate_limit]
|
22
|
+
@endpoint_uri = URI(uk_options[:sandbox] ? SANDBOX_ENDPOINT_URL : PRODUCTION_ENDPOINT_URL)
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.fetch(options = {})
|
26
|
+
new(options).fetch
|
27
|
+
end
|
28
|
+
|
29
|
+
def fetch(uri = @endpoint_uri, request_count = 0)
|
30
|
+
raise_if_invalid!
|
31
|
+
|
32
|
+
request = build_request(uri)
|
33
|
+
response = build_https.request(request)
|
34
|
+
handle_response!(response, request_count)
|
35
|
+
rescue Errno::ECONNRESET, IOError
|
36
|
+
raise if request_count > @rate_limit
|
37
|
+
|
38
|
+
fetch(uri, request_count + 1)
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def raise_if_invalid!
|
44
|
+
raise Error, 'Client ID is missing' if @client_id.empty?
|
45
|
+
raise Error, 'Client secret is missing' if @client_secret.empty?
|
46
|
+
end
|
47
|
+
|
48
|
+
def build_request(uri)
|
49
|
+
request = Net::HTTP::Post.new(uri)
|
50
|
+
request['Content-Type'] = 'application/x-www-form-urlencoded'
|
51
|
+
request.body = build_body
|
52
|
+
request
|
53
|
+
end
|
54
|
+
|
55
|
+
def build_body
|
56
|
+
URI.encode_www_form(
|
57
|
+
scope: SCOPE,
|
58
|
+
grant_type: GRANT_TYPE,
|
59
|
+
client_id: @client_id,
|
60
|
+
client_secret: @client_secret
|
61
|
+
)
|
62
|
+
end
|
63
|
+
|
64
|
+
def build_https
|
65
|
+
https = Net::HTTP.new(@endpoint_uri.host, @endpoint_uri.port)
|
66
|
+
https.use_ssl = true
|
67
|
+
https
|
68
|
+
end
|
69
|
+
|
70
|
+
def parse_response!(response)
|
71
|
+
JSON.parse(response.read_body)
|
72
|
+
end
|
73
|
+
|
74
|
+
def parse_response_location(response)
|
75
|
+
URI.parse(response['Location'])
|
76
|
+
end
|
77
|
+
|
78
|
+
# See https://developer.service.hmrc.gov.uk/api-documentation/docs/authorisation/application-restricted-endpoints
|
79
|
+
def handle_response!(response, request_count)
|
80
|
+
if response.is_a?(Net::HTTPRedirection) && request_count < @rate_limit
|
81
|
+
fetch(parse_response_location(response), request_count + 1)
|
82
|
+
elsif response.code == '200'
|
83
|
+
# {"access_token":"<access_token>","token_type":"bearer","expires_in":14400,"scope":"read:vat"}
|
84
|
+
parse_response!(response)['access_token']
|
85
|
+
else
|
86
|
+
raise Error, "Failed to fetch access token: #{handle_response_error(response, request_count)}"
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def handle_response_error(response, request_count)
|
91
|
+
if response.code.match?(/4\d\d/)
|
92
|
+
# For 4xx codes: {"error":"invalid_client","error_description":"invalid client id or secret"}
|
93
|
+
parse_response!(response)['error_description']
|
94
|
+
elsif request_count >= @rate_limit
|
95
|
+
'rate limit exceeded'
|
96
|
+
else
|
97
|
+
response.read_body
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
data/lib/valvat/lookup/base.rb
CHANGED
@@ -10,6 +10,7 @@ class Valvat
|
|
10
10
|
@vat = Valvat(vat)
|
11
11
|
@options = Valvat::Options(options)
|
12
12
|
@requester = @options[:requester] && Valvat(@options[:requester])
|
13
|
+
@rate_limit = @options[:rate_limit]
|
13
14
|
end
|
14
15
|
|
15
16
|
def perform
|
@@ -40,13 +41,13 @@ class Valvat
|
|
40
41
|
def fetch(uri, limit = 0)
|
41
42
|
response = send_request(uri)
|
42
43
|
|
43
|
-
if response == Net::HTTPRedirection && limit <
|
44
|
+
if response == Net::HTTPRedirection && limit < @rate_limit
|
44
45
|
fetch(URI.parse(response['Location']), limit + 1)
|
45
46
|
else
|
46
47
|
response
|
47
48
|
end
|
48
49
|
rescue Errno::ECONNRESET, IOError
|
49
|
-
raise if limit >
|
50
|
+
raise if limit > @rate_limit
|
50
51
|
|
51
52
|
fetch(uri, limit + 1)
|
52
53
|
end
|
data/lib/valvat/lookup/hmrc.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative 'base'
|
4
|
+
require_relative '../hmrc/access_token'
|
4
5
|
require 'net/http'
|
5
6
|
require 'json'
|
6
7
|
require 'time'
|
@@ -8,16 +9,19 @@ require 'time'
|
|
8
9
|
class Valvat
|
9
10
|
class Lookup
|
10
11
|
class HMRC < Base
|
11
|
-
|
12
|
+
PRODUCTION_ENDPOINT_URL = 'https://api.service.hmrc.gov.uk/organisations/vat/check-vat-number/lookup'
|
13
|
+
SANDBOX_ENDPOINT_URL = 'https://test-api.service.hmrc.gov.uk/organisations/vat/check-vat-number/lookup'
|
12
14
|
HEADERS = {
|
13
15
|
# https://developer.service.hmrc.gov.uk/api-documentation/docs/reference-guide#versioning
|
14
|
-
'Accept' => 'application/vnd.hmrc.
|
16
|
+
'Accept' => 'application/vnd.hmrc.2.0+json'
|
15
17
|
}.freeze
|
16
18
|
|
17
19
|
def perform
|
18
|
-
return { valid: false } unless @options[:uk]
|
20
|
+
return { valid: false } unless @options[:uk].is_a?(Hash)
|
19
21
|
|
20
22
|
parse(fetch(endpoint_uri).body)
|
23
|
+
rescue Valvat::HMRC::AccessToken::Error => e
|
24
|
+
{ error: e }
|
21
25
|
end
|
22
26
|
|
23
27
|
private
|
@@ -25,11 +29,12 @@ class Valvat
|
|
25
29
|
def endpoint_uri
|
26
30
|
endpoint = "/#{@vat.to_s_wo_country}"
|
27
31
|
endpoint += "/#{@requester.to_s_wo_country}" if @requester
|
28
|
-
|
32
|
+
endpoint_url = @options.dig(:uk, :sandbox) ? SANDBOX_ENDPOINT_URL : PRODUCTION_ENDPOINT_URL
|
33
|
+
URI.parse(endpoint_url + endpoint)
|
29
34
|
end
|
30
35
|
|
31
36
|
def build_request(uri)
|
32
|
-
Net::HTTP::Get.new(uri.request_uri,
|
37
|
+
Net::HTTP::Get.new(uri.request_uri, build_headers!)
|
33
38
|
end
|
34
39
|
|
35
40
|
def parse(body)
|
@@ -72,7 +77,8 @@ class Valvat
|
|
72
77
|
'SCHEDULED_MAINTENANCE' => ServiceUnavailable,
|
73
78
|
'SERVER_ERROR' => ServiceUnavailable,
|
74
79
|
'INVALID_REQUEST' => InvalidRequester,
|
75
|
-
'GATEWAY_TIMEOUT' => Timeout
|
80
|
+
'GATEWAY_TIMEOUT' => Timeout,
|
81
|
+
'INVALID_CREDENTIALS' => AuthorizationError
|
76
82
|
}.freeze
|
77
83
|
|
78
84
|
def build_fault(raw)
|
@@ -82,6 +88,14 @@ class Valvat
|
|
82
88
|
exception = FAULTS[fault] || UnknownLookupError
|
83
89
|
{ error: exception.new("#{fault}#{raw['message'] ? " (#{raw['message']})" : ''}", self.class) }
|
84
90
|
end
|
91
|
+
|
92
|
+
def build_headers!
|
93
|
+
HEADERS.merge('Authorization' => "Bearer #{fetch_access_token!}")
|
94
|
+
end
|
95
|
+
|
96
|
+
def fetch_access_token!
|
97
|
+
@fetch_access_token ||= Valvat::HMRC::AccessToken.fetch(@options)
|
98
|
+
end
|
85
99
|
end
|
86
100
|
end
|
87
101
|
end
|
data/lib/valvat/options.rb
CHANGED
@@ -18,11 +18,26 @@ class Valvat
|
|
18
18
|
@options[key] ||= @options[deprecated]
|
19
19
|
end
|
20
20
|
end
|
21
|
+
|
22
|
+
check_uk_key
|
21
23
|
end
|
22
24
|
|
23
25
|
def [](key)
|
24
26
|
@options.key?(key) ? @options[key] : Valvat.config[key]
|
25
27
|
end
|
28
|
+
|
29
|
+
def dig(*keys)
|
30
|
+
@options.dig(*keys).nil? ? Valvat.config.dig(*keys) : @options.dig(*keys)
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def check_uk_key
|
36
|
+
return if @options[:uk] != true || silence
|
37
|
+
|
38
|
+
puts 'DEPRECATED: The option :uk is not allowed to be set to `true` anymore. ' \
|
39
|
+
'Using the HMRC API requires authentication credentials.'
|
40
|
+
end
|
26
41
|
end
|
27
42
|
|
28
43
|
def self.Options(options)
|
data/lib/valvat/utils.rb
CHANGED
@@ -45,5 +45,10 @@ class Valvat
|
|
45
45
|
val.is_a?(Hash) ? deep_symbolize_keys(val) : val
|
46
46
|
end
|
47
47
|
end
|
48
|
+
|
49
|
+
def self.deep_merge(original_hash, hash_to_merge)
|
50
|
+
merger = proc { |_key, v1, v2| v1.is_a?(Hash) && v2.is_a?(Hash) ? v1.merge(v2, &merger) : v2 }
|
51
|
+
original_hash.merge(hash_to_merge, &merger)
|
52
|
+
end
|
48
53
|
end
|
49
54
|
end
|
data/lib/valvat/version.rb
CHANGED
data.tar.gz.sig
CHANGED
Binary file
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: valvat
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sebastian Munz
|
@@ -34,7 +34,7 @@ cert_chain:
|
|
34
34
|
4+Gn3/xNnr5M7CdCGOG96P+gnx508xxiuMas8SvHl0/WB4kFpSGTH3uKzFh9fF+I
|
35
35
|
36KG/gudYiTEZqWbjFIh4jJceFclxM/L
|
36
36
|
-----END CERTIFICATE-----
|
37
|
-
date: 2024-
|
37
|
+
date: 2024-12-06 00:00:00.000000000 Z
|
38
38
|
dependencies:
|
39
39
|
- !ruby/object:Gem::Dependency
|
40
40
|
name: rexml
|
@@ -42,7 +42,7 @@ dependencies:
|
|
42
42
|
requirements:
|
43
43
|
- - ">="
|
44
44
|
- !ruby/object:Gem::Version
|
45
|
-
version: 3.
|
45
|
+
version: 3.3.6
|
46
46
|
- - "<"
|
47
47
|
- !ruby/object:Gem::Version
|
48
48
|
version: 4.0.0
|
@@ -52,7 +52,7 @@ dependencies:
|
|
52
52
|
requirements:
|
53
53
|
- - ">="
|
54
54
|
- !ruby/object:Gem::Version
|
55
|
-
version: 3.
|
55
|
+
version: 3.3.6
|
56
56
|
- - "<"
|
57
57
|
- !ruby/object:Gem::Version
|
58
58
|
version: 4.0.0
|
@@ -93,6 +93,7 @@ files:
|
|
93
93
|
- lib/valvat/checksum/si.rb
|
94
94
|
- lib/valvat/configuration.rb
|
95
95
|
- lib/valvat/error.rb
|
96
|
+
- lib/valvat/hmrc/access_token.rb
|
96
97
|
- lib/valvat/local.rb
|
97
98
|
- lib/valvat/locales/bg.yml
|
98
99
|
- lib/valvat/locales/ca.yml
|
metadata.gz.sig
CHANGED
Binary file
|