valvat 1.1.5 → 1.2.1
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 +2 -2
- data/.rubocop.yml +9 -3
- data/CHANGES.md +19 -2
- data/MIT-LICENSE +1 -1
- data/README.md +48 -33
- data/lib/valvat/checksum/es.rb +49 -16
- 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 +87 -0
- data/lib/valvat/lookup/vies.rb +99 -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 +41 -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 +259 -71
- data/valvat.gemspec +3 -1
- data.tar.gz.sig +0 -0
- metadata +32 -14
- metadata.gz.sig +0 -0
- data/lib/valvat/lookup/fault.rb +0 -44
- 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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9b0b39e1acd3284cb03b0a5cd1a2beec5dc11b7539491829ebf56086c7a40413
|
4
|
+
data.tar.gz: 15c2ae43ec8fb9ad276d7cd473c51c1c505bdc53e1049ddc6554fea15a2a0c4d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c6342f5e92bc881b491ac326a17b6327c622f639476bbffa60876b89aa1a1f6ab30848d56f58202567b51a930037e715dcbe10e8cadc021d4f72a5560227df84
|
7
|
+
data.tar.gz: b029ff8192f35b77abf95bdc5adcff6ce3afbb6895b46d2dcb51d9d062c46de7d9e64a9e6653e16201938b6f3b32976ea83656b08551619d5d89d9894c823dac
|
checksums.yaml.gz.sig
CHANGED
Binary file
|
data/.github/workflows/ruby.yml
CHANGED
data/.rubocop.yml
CHANGED
@@ -9,10 +9,16 @@ Metrics/BlockLength:
|
|
9
9
|
- spec/**/*_spec.rb
|
10
10
|
|
11
11
|
Style/Documentation:
|
12
|
-
|
12
|
+
Enabled: false
|
13
13
|
|
14
14
|
RSpec/MultipleExpectations:
|
15
|
-
|
15
|
+
Enabled: false
|
16
16
|
|
17
17
|
Style/StringChars:
|
18
|
-
Enabled: false
|
18
|
+
Enabled: false
|
19
|
+
|
20
|
+
RSpec/NestedGroups:
|
21
|
+
Max: 4
|
22
|
+
|
23
|
+
RSpec/ExampleLength:
|
24
|
+
Enabled: false
|
data/CHANGES.md
CHANGED
@@ -1,7 +1,24 @@
|
|
1
1
|
|
2
2
|
### dev
|
3
3
|
|
4
|
-
[full changelog](http://github.com/yolk/valvat/compare/v1.1
|
4
|
+
[full changelog](http://github.com/yolk/valvat/compare/v1.2.1...master)
|
5
|
+
|
6
|
+
### 1.2.1 / 2022-10-05
|
7
|
+
|
8
|
+
[full changelog](http://github.com/yolk/valvat/compare/v1.2.0...v1.2.1)
|
9
|
+
|
10
|
+
* Added missing dependency rexml #117
|
11
|
+
* Lookup: Retry IOError
|
12
|
+
* Lookup: require 'date' in VIES and 'time' in HMRC
|
13
|
+
|
14
|
+
### 1.2.0 / 2022-09-30
|
15
|
+
|
16
|
+
[full changelog](http://github.com/yolk/valvat/compare/v1.1.5...v1.2.0)
|
17
|
+
|
18
|
+
* Implemented lookup of VAT numbers from the UK (via HMRC api and only with :uk option set to true) (by [Adrien Rey-Jarthon](https://github.com/jarthod))
|
19
|
+
* Remimplemented VIES lookup using only nethttp (removes dependency on savon)
|
20
|
+
* Deprecate require 'valvat/local'. Please require 'valvat' directly.
|
21
|
+
* Apply more rules to spanish VAT numbers on checksum validation #115 (by [Thomas Scalise](https://github.com/KirtashW17))
|
5
22
|
|
6
23
|
### 1.1.5 / 2022-09-14
|
7
24
|
|
@@ -22,7 +39,7 @@
|
|
22
39
|
|
23
40
|
[full changelog](http://github.com/yolk/valvat/compare/v1.1.2...v1.1.3)
|
24
41
|
|
25
|
-
* Handle Savon::HTTPError and Savon::UnknownOperationError as
|
42
|
+
* Handle Savon::HTTPError and Savon::UnknownOperationError as LookupError and throw Valvat::HTTPError and Valvat::OperationUnknown instead.
|
26
43
|
|
27
44
|
### 1.1.2 / 2021-10-29
|
28
45
|
|
data/MIT-LICENSE
CHANGED
data/README.md
CHANGED
@@ -7,7 +7,7 @@ Validates european vat numbers. Standalone or as a ActiveModel validator.
|
|
7
7
|
|
8
8
|
## A note on Brexit
|
9
9
|
|
10
|
-
Valvat supports validating VAT-IDs from the UK by syntax
|
10
|
+
Valvat supports validating VAT-IDs from the UK by syntax, checksum and using the HMRC API (for backwards compatibility only with the `:uk` option set to true). Validation against the VIES web service stopped working early 2021.
|
11
11
|
|
12
12
|
Northern Ireland received its own VAT number prefix - XI which is supported by VIES web service so any XI-prefixed VAT numbers should be validated as any EU VAT number.
|
13
13
|
|
@@ -15,8 +15,10 @@ Northern Ireland received its own VAT number prefix - XI which is supported by V
|
|
15
15
|
|
16
16
|
* Simple syntax verification
|
17
17
|
* Lookup via the VIES web service
|
18
|
+
* (Optional) lookup via the HMRC web service (for UK VAT numbers)
|
18
19
|
* ActiveModel/Rails integration
|
19
20
|
* Works standalone without ActiveModel
|
21
|
+
* Minimal runtime dependencies
|
20
22
|
* I18n locales for language specific error messages in English, German, French, Spanish, Italian, Portuguese, Polish, Swedish, Dutch, Danish, Czech, Slovakian, Hungarian, Bulgarian, Romanian, Latvian, Catalan, Norwegian, and Finnish.
|
21
23
|
* *Experimental* checksum verification
|
22
24
|
|
@@ -30,13 +32,7 @@ Add it to your Gemfile:
|
|
30
32
|
gem 'valvat'
|
31
33
|
```
|
32
34
|
|
33
|
-
|
34
|
-
|
35
|
-
```ruby
|
36
|
-
gem 'valvat', require: 'valvat/local'
|
37
|
-
```
|
38
|
-
|
39
|
-
In any case run:
|
35
|
+
And run:
|
40
36
|
|
41
37
|
$ bundle
|
42
38
|
|
@@ -60,23 +56,32 @@ Valvat::Syntax.validate("DE345789003")
|
|
60
56
|
# => true or false
|
61
57
|
```
|
62
58
|
|
63
|
-
## Validate against the VIES web service
|
59
|
+
## Validate against the VIES / HMRC web service
|
64
60
|
|
65
|
-
To check if the given vat number exists via the VIES web service:
|
61
|
+
To check if the given vat number exists via the VIES or HMRC web service:
|
66
62
|
|
67
63
|
```ruby
|
68
64
|
Valvat.new("DE345789003").exists?
|
69
65
|
# => true or false or nil
|
70
66
|
```
|
71
67
|
|
72
|
-
Or to lookup a vat number string directly
|
68
|
+
Or to lookup a vat number string directly:
|
73
69
|
|
74
70
|
```ruby
|
75
71
|
Valvat::Lookup.validate("DE345789003")
|
76
72
|
# => true or false or nil
|
77
73
|
```
|
78
74
|
|
79
|
-
|
75
|
+
To keep backwards compatibility lookups of UK VAT numbers against the HMRC API are only performed with the option `:uk` set to true.
|
76
|
+
|
77
|
+
```ruby
|
78
|
+
Valvat::Lookup.validate("GB553557881", uk: true)
|
79
|
+
# => true or false or nil
|
80
|
+
```
|
81
|
+
|
82
|
+
Without this option the lookup of UK VAT number always returns `false`.
|
83
|
+
|
84
|
+
*IMPORTANT* Keep in mind that the web service might be offline at some time for all or some member states. If this happens `exists?` or `Valvat::Lookup.validate` will return `nil`. See *Handling of maintenance errors* for further details.
|
80
85
|
|
81
86
|
### Details & request identifier
|
82
87
|
|
@@ -85,13 +90,13 @@ If you need all details and not only if the VAT is valid, pass {detail: true} as
|
|
85
90
|
```ruby
|
86
91
|
Valvat.new("IE6388047V").exists?(detail: true)
|
87
92
|
=> {
|
88
|
-
:country_code=>"IE", :vat_number => "6388047V", :valid => true,
|
89
|
-
:request_date => Date.today, :name=>"GOOGLE IRELAND LIMITED",
|
90
|
-
:address=>"1ST & 2ND FLOOR ,GORDON HOUSE ,BARROW STREET ,DUBLIN 4"
|
93
|
+
:country_code=> "IE", :vat_number => "6388047V", :valid => true,
|
94
|
+
:request_date => Date.today, :name=> "GOOGLE IRELAND LIMITED",
|
95
|
+
:address=> "1ST & 2ND FLOOR ,GORDON HOUSE ,BARROW STREET ,DUBLIN 4"
|
91
96
|
} or false or nil
|
92
97
|
```
|
93
98
|
|
94
|
-
According to EU law, or at least as Austria sees it, it's mandatory to verify the VAT number of every new customer, but also to check the VAT number periodicaly. To prove that you have checked the VAT number, the
|
99
|
+
According to EU law, or at least as Austria sees it, it's mandatory to verify the VAT number of every new customer, but also to check the VAT number periodicaly. To prove that you have checked the VAT number, the web service can return a `request_identifier`.
|
95
100
|
|
96
101
|
To receive a `request_identifier` you need to pass your own VAT number in the options hash. In this example, Google (VAT IE6388047V) is checking the validity of eBays VAT number (LU21416127)
|
97
102
|
|
@@ -107,7 +112,14 @@ Valvat.new("LU21416127").exists?(requester: "IE6388047V")
|
|
107
112
|
|
108
113
|
If the given `requester` is invalid, a `Valvat::InvalidRequester` error is thrown.
|
109
114
|
|
110
|
-
|
115
|
+
When requesting a `request_identifier` for a GB VAT number, the requester must be your own GB number; a EU VAT number won't work.
|
116
|
+
|
117
|
+
Note that when validating UK VAT numbers using the HMRC service, the detail output is modified to match the one from VIES more closely with slight differences remaining:
|
118
|
+
|
119
|
+
1. The `request_date` will actually be a (more precise) `Time` instead of a `Date`
|
120
|
+
2. The `address` string will join lines using `\n` instead of `,` so it's more acurate and can be displayed nicely.
|
121
|
+
|
122
|
+
### Handling of maintenance errors
|
111
123
|
|
112
124
|
From time to time the VIES web service for one or all member states is down for maintenance. To handle this kind of temporary errors, `Valvat::Lookup#validate` returns `nil` by default to indicate that there is no way at the moment to say if the given VAT is valid or not. You should revalidate the VAT later. If you prefer an error, use the `raise_error` option:
|
113
125
|
|
@@ -119,9 +131,9 @@ This raises `Valvat::ServiceUnavailable` or `Valvat::MemberStateUnavailable` ins
|
|
119
131
|
|
120
132
|
Visit [http://ec.europa.eu/taxation_customs/vies/viesspec.do](http://ec.europa.eu/taxation_customs/vies/viesspec.do) for more accurate information at what time the service for a specific member state will be down.
|
121
133
|
|
122
|
-
### Handling of other
|
134
|
+
### Handling of other errors
|
123
135
|
|
124
|
-
All other errors accuring while validating against the
|
136
|
+
All other errors accuring while validating against the web service are raised and must be handled by you. These include:
|
125
137
|
|
126
138
|
* `Valvat::InvalidRequester`
|
127
139
|
* `Valvat::BlockedError`
|
@@ -137,23 +149,19 @@ Valvat.new("IE6388047V").exists?(raise_error: false)
|
|
137
149
|
|
138
150
|
This will return `nil` instead of raising a known error.
|
139
151
|
|
140
|
-
### Set options for the
|
152
|
+
### Set options for the Net::HTTP client
|
141
153
|
|
142
|
-
Use the `:
|
143
|
-
|
144
|
-
```ruby
|
145
|
-
Valvat.new("IE6388047V").exists?(savon: { log: true })
|
146
|
-
```
|
154
|
+
Use the `:http` key to set options for the http client. These options are directly passed to `Net::HTTP.start`.
|
147
155
|
|
148
|
-
|
156
|
+
For example to set timeouts:
|
149
157
|
|
150
158
|
```ruby
|
151
|
-
Valvat.new("IE6388047V").exists?(
|
159
|
+
Valvat.new("IE6388047V").exists?(http: { open_timeout: 10, read_timeout: 10 })
|
152
160
|
```
|
153
161
|
|
154
162
|
### Skip local validation before lookup
|
155
163
|
|
156
|
-
To prevent unnecessary requests, valvat performs a local syntax check before making the request to the
|
164
|
+
To prevent unnecessary requests, valvat performs a local syntax check before making the request to the web service. If you want to skip this step (for any reason), set the `:skip_local_validation` option to `true`.
|
157
165
|
|
158
166
|
## Experimental checksum verification
|
159
167
|
|
@@ -164,7 +172,7 @@ Valvat.new("DE345789003").valid_checksum?
|
|
164
172
|
# => true or false
|
165
173
|
```
|
166
174
|
|
167
|
-
These results are more valuable than a simple syntax check, but keep in mind: they can not replace a lookup via VIES.
|
175
|
+
These results are more valuable than a simple syntax check, but keep in mind: they can not replace a lookup via VIES or HMRC.
|
168
176
|
|
169
177
|
*IMPORTANT* This feature was tested against all vat numbers I could get my hand on, but it is still marked as *experimental* because these calculations are not documented and may return wrong results.
|
170
178
|
|
@@ -199,13 +207,19 @@ end
|
|
199
207
|
|
200
208
|
### Additional lookup validation
|
201
209
|
|
202
|
-
To additionally perform
|
210
|
+
To additionally perform an lookup via VIES:
|
203
211
|
|
204
212
|
```ruby
|
205
213
|
validates :vat_number, valvat: { lookup: true }
|
206
214
|
```
|
207
215
|
|
208
|
-
|
216
|
+
To also perform an lookup via HMRC for UK VAT numbers:
|
217
|
+
|
218
|
+
```ruby
|
219
|
+
validates :vat_number, valvat: { lookup: { uk: true } }
|
220
|
+
```
|
221
|
+
|
222
|
+
By default this will validate to true if the web service is down. To fail in this case simply add the `:fail_if_down` option:
|
209
223
|
|
210
224
|
```ruby
|
211
225
|
validates :vat_number, valvat: { lookup: { fail_if_down: true } }
|
@@ -214,7 +228,7 @@ validates :vat_number, valvat: { lookup: { fail_if_down: true } }
|
|
214
228
|
You can pass in any options accepted by `Valvat::Lookup#validate`:
|
215
229
|
|
216
230
|
```ruby
|
217
|
-
validates :vat_number, valvat: { lookup: { raise_error: true,
|
231
|
+
validates :vat_number, valvat: { lookup: { raise_error: true, http: { read_timeout: 12 } } }
|
218
232
|
```
|
219
233
|
|
220
234
|
### Additional (and experimental) checksum validation
|
@@ -305,6 +319,7 @@ There seems to be a problem when using the VIES service over IPv6. Sadly this is
|
|
305
319
|
## Links
|
306
320
|
|
307
321
|
* [VIES web service](http://ec.europa.eu/taxation_customs/vies)
|
322
|
+
* [HMRC web service](https://developer.service.hmrc.gov.uk/api-documentation/docs/api/service/vat-registered-companies-api/1.0)
|
308
323
|
* [European vat number formats (german)](http://bzst.de/DE/Steuern_International/USt_Identifikationsnummer/Merkblaetter/Aufbau_USt_IdNr.html)
|
309
324
|
* [European vat number formats on Wikipedia](http://en.wikipedia.org/wiki/European_Union_Value_Added_Tax)
|
310
325
|
|
@@ -314,7 +329,7 @@ https://github.com/yolk/valvat/graphs/contributors
|
|
314
329
|
|
315
330
|
## BlaBla
|
316
331
|
|
317
|
-
Copyright (c) 2011-2022
|
332
|
+
Copyright (c) 2011-2022 mite GmbH
|
318
333
|
|
319
334
|
Beyond that, the implementation is licensed under the MIT License.
|
320
335
|
|
data/lib/valvat/checksum/es.rb
CHANGED
@@ -4,32 +4,53 @@ class Valvat
|
|
4
4
|
module Checksum
|
5
5
|
class ES < Base
|
6
6
|
NATURAL_PERSON_CHARS = %w[T R W A G M Y F P D X B N J Z S Q V H L C K E].freeze
|
7
|
-
NATURAL_PERSON_EXP = /\A
|
7
|
+
NATURAL_PERSON_EXP = /\A[KLMXYZ\d]/.freeze
|
8
8
|
LEGAL_PERSON_CHARS = [false] + %w[A B C D E F G H I J]
|
9
|
-
LEGAL_PERSON_EXP = /\A[NPQRSW]\d{7}[ABCDEFGHIJ]\Z/.freeze
|
10
9
|
NIE_DIGIT_BY_LETTER = %w[X Y Z].freeze
|
10
|
+
GIVEN_CD_IS_A_LETTER_EXP = /[A-Z]\Z/.freeze
|
11
|
+
LEGAL_PERSON_EXP = /\A[ABCDEFGHJUVNPQRSW]/.freeze
|
12
|
+
CIF_MUST_BE_A_LETTER_EXP = /\A[NPQRSW]/.freeze
|
13
|
+
CIF_MUST_BE_A_NUMBER_EXP = /\A[HJUV]/.freeze
|
14
|
+
SPECIAL_NIF_EXP = /\A[KLM]/.freeze
|
11
15
|
|
12
|
-
def
|
13
|
-
|
16
|
+
def validate
|
17
|
+
passes_special_validations? && possible_check_digits.include?(given_check_digit)
|
14
18
|
end
|
15
19
|
|
16
|
-
|
20
|
+
private
|
21
|
+
|
22
|
+
def passes_special_validations?
|
23
|
+
!(
|
24
|
+
# [KLM]: CD first two numerical digits must be between 01 and 56 (both inclusive)
|
25
|
+
(vat.to_s_wo_country =~ SPECIAL_NIF_EXP &&
|
26
|
+
vat.to_s_wo_country[1..2].to_i > 56) or vat.to_s_wo_country[1..2].to_i < 0o1 ||
|
27
|
+
# Exceptions: X0000000T, 00000001R, 00000000T, 99999999R are invalid.
|
28
|
+
%w[X0000000T 00000001R 00000000T 99999999R].include?(vat.to_s_wo_country)
|
29
|
+
)
|
30
|
+
end
|
31
|
+
|
32
|
+
def given_check_digit
|
33
|
+
given_cd_is_a_letter? ? str_wo_country[-1] : super
|
34
|
+
end
|
35
|
+
|
36
|
+
def possible_check_digits
|
37
|
+
natural_person? ? possible_cd_natural_person : possible_cds_legal_person
|
38
|
+
end
|
39
|
+
|
40
|
+
def possible_cd_natural_person
|
17
41
|
letter = vat.to_s_wo_country[0]
|
18
42
|
nie_digit = NIE_DIGIT_BY_LETTER.index(letter)
|
19
|
-
NATURAL_PERSON_CHARS["#{nie_digit}#{figures_str}".to_i.modulo(23)]
|
43
|
+
[NATURAL_PERSON_CHARS["#{nie_digit}#{figures_str}".to_i.modulo(23)]]
|
20
44
|
end
|
21
45
|
|
22
|
-
def
|
46
|
+
def possible_cds_legal_person
|
23
47
|
chk = 10 - sum_of_figures_for_at_es_it_se(reverse_ints: true).modulo(10)
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
(chk == 10 ? 0 : chk)
|
48
|
+
possible_check_digits = []
|
49
|
+
possible_check_digits << LEGAL_PERSON_CHARS[chk] if cd_can_be_a_letter?
|
50
|
+
if cd_can_be_a_num?
|
51
|
+
possible_check_digits << (chk == 10 ? 0 : chk)
|
28
52
|
end
|
29
|
-
|
30
|
-
|
31
|
-
def given_check_digit
|
32
|
-
person? ? str_wo_country[-1] : super
|
53
|
+
possible_check_digits
|
33
54
|
end
|
34
55
|
|
35
56
|
def str_wo_country
|
@@ -46,7 +67,19 @@ class Valvat
|
|
46
67
|
end
|
47
68
|
|
48
69
|
def legal_foreign_person?
|
49
|
-
!!(vat.to_s_wo_country =~
|
70
|
+
!!(vat.to_s_wo_country =~ FOREIGN_LEGAL_PERSON_EXP)
|
71
|
+
end
|
72
|
+
|
73
|
+
def cd_can_be_a_letter?
|
74
|
+
vat.to_s_wo_country !~ CIF_MUST_BE_A_NUMBER_EXP
|
75
|
+
end
|
76
|
+
|
77
|
+
def cd_can_be_a_num?
|
78
|
+
vat.to_s_wo_country !~ CIF_MUST_BE_A_LETTER_EXP
|
79
|
+
end
|
80
|
+
|
81
|
+
def given_cd_is_a_letter?
|
82
|
+
!!(vat.to_s_wo_country =~ GIVEN_CD_IS_A_LETTER_EXP)
|
50
83
|
end
|
51
84
|
end
|
52
85
|
end
|
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, IOError
|
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,87 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base'
|
4
|
+
require 'net/http'
|
5
|
+
require 'json'
|
6
|
+
require 'time'
|
7
|
+
|
8
|
+
class Valvat
|
9
|
+
class Lookup
|
10
|
+
class HMRC < Base
|
11
|
+
ENDPOINT_URL = 'https://api.service.hmrc.gov.uk/organisations/vat/check-vat-number/lookup'
|
12
|
+
HEADERS = {
|
13
|
+
# https://developer.service.hmrc.gov.uk/api-documentation/docs/reference-guide#versioning
|
14
|
+
'Accept' => 'application/vnd.hmrc.1.0+json'
|
15
|
+
}.freeze
|
16
|
+
|
17
|
+
def perform
|
18
|
+
return { valid: false } unless @options[:uk] == true
|
19
|
+
|
20
|
+
parse(fetch(endpoint_uri).body)
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def endpoint_uri
|
26
|
+
endpoint = "/#{@vat.to_s_wo_country}"
|
27
|
+
endpoint += "/#{@requester.to_s_wo_country}" if @requester
|
28
|
+
URI.parse(ENDPOINT_URL + endpoint)
|
29
|
+
end
|
30
|
+
|
31
|
+
def build_request(uri)
|
32
|
+
Net::HTTP::Get.new(uri.request_uri, HEADERS)
|
33
|
+
end
|
34
|
+
|
35
|
+
def parse(body)
|
36
|
+
convert(JSON.parse(body))
|
37
|
+
end
|
38
|
+
|
39
|
+
# Return a similar format to VIES
|
40
|
+
# Main differences are:
|
41
|
+
# - request_date is a (more precise) Time instead of Date
|
42
|
+
# - address is newline separated instead of coma (also more precise)
|
43
|
+
def convert(raw)
|
44
|
+
return build_fault(raw) if raw.key?('code')
|
45
|
+
|
46
|
+
{
|
47
|
+
address: format_address(raw.dig('target', 'address')),
|
48
|
+
country_code: raw.dig('target', 'address', 'countryCode'),
|
49
|
+
name: raw.dig('target', 'name'),
|
50
|
+
vat_number: raw.dig('target', 'vatNumber'), valid: true
|
51
|
+
}.tap do |hash|
|
52
|
+
hash[:request_date] = Time.parse(raw['processingDate']) if raw.key?('processingDate')
|
53
|
+
hash[:request_identifier] = raw['consultationNumber'] if raw.key?('consultationNumber')
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Example raw address from the API:
|
58
|
+
# {
|
59
|
+
# "line1": "HM REVENUE AND CUSTOMS",
|
60
|
+
# "line2": "RUBY HOUSE",
|
61
|
+
# "line3": "8 RUBY PLACE",
|
62
|
+
# "line4": "ABERDEEN",
|
63
|
+
# "postcode": "AB10 1ZP",
|
64
|
+
# "countryCode": "GB"
|
65
|
+
# }
|
66
|
+
def format_address(address)
|
67
|
+
address&.values&.join("\n")
|
68
|
+
end
|
69
|
+
|
70
|
+
FAULTS = {
|
71
|
+
'MESSAGE_THROTTLED_OUT' => RateLimitError,
|
72
|
+
'SCHEDULED_MAINTENANCE' => ServiceUnavailable,
|
73
|
+
'SERVER_ERROR' => ServiceUnavailable,
|
74
|
+
'INVALID_REQUEST' => InvalidRequester,
|
75
|
+
'GATEWAY_TIMEOUT' => Timeout
|
76
|
+
}.freeze
|
77
|
+
|
78
|
+
def build_fault(raw)
|
79
|
+
fault = raw['code']
|
80
|
+
return { valid: false } if fault == 'NOT_FOUND'
|
81
|
+
|
82
|
+
exception = FAULTS[fault] || UnknownLookupError
|
83
|
+
{ error: exception.new("#{fault}#{raw['message'] ? " (#{raw['message']})" : ''}", self.class) }
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|