valvat 1.1.5 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 51672c127e1f949767059172b89bcadfc148888809889c8cd0b425031c002132
4
- data.tar.gz: '04189c3b112baeff2001a6f5b6bdccb0de3052e66d05636e597fb6bde175e94c'
3
+ metadata.gz: d8adbca62e2ebbf198c817bfed2cf8dc2d375a24cb146dff52ce42a6dd59750d
4
+ data.tar.gz: 84949112505cff60333ff382583b54f4b4c20cb86e59bf5cbb1c16616c1c3ce7
5
5
  SHA512:
6
- metadata.gz: f330e05a5c50652036ed2b445884e202c72708d10210fd09528b3f54d54afe5de3acf59d84af4bea66aba76d0f98ee4d7464ce9c0399c80644257368b4880472
7
- data.tar.gz: b730f20e4c950324d99111cb73add8dc833d56587554bc62fd39e6576bd6a41a914c4e68e1666a6ed1ed7bdc2b36881fc6e69b48ca30d02a76bd9e01a6e8b446
6
+ metadata.gz: d2c2cb07112932730644124d140dc29a8e1ffc2e0b590d67ef1ff9f24dfb9c327fc72c189a8ebccdb983c04271c5cabebfa210bc20a61d8b9788c489135d58f9
7
+ data.tar.gz: 543a2f012caac4dfb874d9c5cca697b3cde38bbb16ca646ff87e66020d913f58094890ceed95d0ffa39637d4c538253358fe0f6ae4ea0a045fe6de4fd9f8585b
checksums.yaml.gz.sig CHANGED
Binary file
@@ -2,12 +2,12 @@ name: Specs
2
2
 
3
3
  on:
4
4
  push:
5
- branches: [ master ]
6
5
  paths-ignore:
7
6
  - 'README.md'
8
7
  - 'CHANGES.md'
9
8
  pull_request:
10
- branches: [ master ]
9
+ schedule:
10
+ - cron: '52 8 * * *'
11
11
 
12
12
  jobs:
13
13
  test:
data/.rubocop.yml CHANGED
@@ -9,10 +9,16 @@ Metrics/BlockLength:
9
9
  - spec/**/*_spec.rb
10
10
 
11
11
  Style/Documentation:
12
- Enabled: false
12
+ Enabled: false
13
13
 
14
14
  RSpec/MultipleExpectations:
15
- Enabled: false
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,16 @@
1
1
 
2
2
  ### dev
3
3
 
4
- [full changelog](http://github.com/yolk/valvat/compare/v1.1.5...master)
4
+ [full changelog](http://github.com/yolk/valvat/compare/v1.2.0...master)
5
+
6
+ ### 1.2.0 / 2022-09-30
7
+
8
+ [full changelog](http://github.com/yolk/valvat/compare/v1.1.5...v1.2.0)
9
+
10
+ * 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))
11
+ * Remimplemented VIES lookup using only nethttp (removes dependency on savon)
12
+ * Deprecate require 'valvat/local'. Please require 'valvat' directly.
13
+ * Apply more rules to spanish VAT numbers on checksum validation #115 (by [Thomas Scalise](https://github.com/KirtashW17))
5
14
 
6
15
  ### 1.1.5 / 2022-09-14
7
16
 
@@ -22,7 +31,7 @@
22
31
 
23
32
  [full changelog](http://github.com/yolk/valvat/compare/v1.1.2...v1.1.3)
24
33
 
25
- * Handle Savon::HTTPError and Savon::UnknownOperationError as ViesError and throw Valvat::HTTPError and Valvat::OperationUnknown instead.
34
+ * Handle Savon::HTTPError and Savon::UnknownOperationError as LookupError and throw Valvat::HTTPError and Valvat::OperationUnknown instead.
26
35
 
27
36
  ### 1.1.2 / 2021-10-29
28
37
 
data/MIT-LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2011 Yolk Sebastian Munz & Julia Soergel GbR
1
+ Copyright (c) 2011-2022 mite GmbH
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
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 & checksum for now. Validation against the VIES web service will probably stop working (and return `false`) in 2021.
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
+ * No 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
- To use less memory (~0.5 mb vs. ~3.5 mb) and load only the local verification functionality – and not the remote lookup with VIES – add it like this instead:
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 via VIES web service:
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
- *IMPORTANT* Keep in mind that the VIES 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 VIES maintenance errors* for further details.
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 VIES Web service can return a `request_identifier`.
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
- ### Handling of VIES maintenance errors
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 VIES errors
134
+ ### Handling of other errors
123
135
 
124
- All other errors accuring while validating against the VIES web service are raised and must be handled by you. These include:
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 savon client
152
+ ### Set options for the Net::HTTP client
141
153
 
142
- Use the `:savon` key to set options for the used savon SOAP client. For example to log all requests:
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
- Or to use higher timeouts for the requests:
156
+ For example to set timeouts:
149
157
 
150
158
  ```ruby
151
- Valvat.new("IE6388047V").exists?(savon: {open_timeout: 10, read_timeout: 10})
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 VIES web service. If you want to skip this step (for any reason), set the `:skip_local_validation` option to `true`.
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 a lookup via VIES:
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
- By default this will validate to true if the VIES web service is down. To fail in this case simply add the `:fail_if_down` option:
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, savon: { log: 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 Yolk Sebastian Munz & Julia Soergel GbR
332
+ Copyright (c) 2011-2022 mite GmbH
318
333
 
319
334
  Beyond that, the implementation is licensed under the MIT License.
320
335
 
@@ -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([KLMXYZ\d])/.freeze
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 check_digit
13
- natural_person? ? check_digit_natural_person : check_digit_legal_person
16
+ def validate
17
+ passes_special_validations? && possible_check_digits.include?(given_check_digit)
14
18
  end
15
19
 
16
- def check_digit_natural_person
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 check_digit_legal_person
46
+ def possible_cds_legal_person
23
47
  chk = 10 - sum_of_figures_for_at_es_it_se(reverse_ints: true).modulo(10)
24
- if legal_foreign_person?
25
- LEGAL_PERSON_CHARS[chk]
26
- else
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
- end
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 =~ LEGAL_PERSON_EXP)
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 ViesError < Error
7
- def initialize(faultstring = 'UNKNOWN', exception = nil)
8
- @faultstring = faultstring || exception.inspect
9
- @exception = exception
10
- super(faultstring)
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 VIES web service returned the error '#{@faultstring}'."
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
- ViesMaintenanceError = Class.new(ViesError)
21
+ MaintenanceError = Class.new(LookupError)
22
22
 
23
- ServiceUnavailable = Class.new(ViesMaintenanceError)
24
- MemberStateUnavailable = Class.new(ViesMaintenanceError)
23
+ ServiceUnavailable = Class.new(MaintenanceError)
24
+ MemberStateUnavailable = Class.new(MaintenanceError)
25
25
 
26
- OperationUnknown = Class.new(ViesError)
27
- HTTPError = Class.new(ViesError)
28
- Timeout = Class.new(ViesError)
29
- InvalidRequester = Class.new(ViesError)
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
- UnknownViesError = Class.new(ViesError)
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
- class Valvat
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
- attr_reader :raw, :vat_country_code, :to_s_wo_country
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