europe 0.0.27 → 0.0.28
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 +12 -0
- data/CHANGELOG.md +4 -0
- data/README.md +29 -0
- data/lib/europe/vat/batch.rb +158 -0
- data/lib/europe/vat/rates.rb +2 -2
- data/lib/europe/vat/vat.rb +29 -0
- data/lib/europe/version.rb +1 -1
- data/test/europe/vat/batch_validation_test.rb +203 -0
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 988b8443805f27f2cb40846b2a2cef3cee76ac464f5c134f833c0ba63acdfce7
|
|
4
|
+
data.tar.gz: '087c687254747b8d653dad7f33de58f52474acfe5211b8a17a1e3be8d3d12fcb'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 70a9e075c5b0c73d8472eb32729c6641a8a365a728be765f5b567516fface3113a2d6fd7751408af1c4aab67e6455a2b138bb92e99fbec41436ce4ae7fd5eb06
|
|
7
|
+
data.tar.gz: b03c256f4cb9e82129fd549dd57d38681df03877df510b6f224c25f9ddfa2125a44cb1279868d58f487d45971100461cf74722527437552d2db941e0921fc839
|
data/.rubocop.yml
CHANGED
|
@@ -19,3 +19,15 @@ Style/CaseLikeIf:
|
|
|
19
19
|
|
|
20
20
|
Naming/PredicateMethod:
|
|
21
21
|
Enabled: false
|
|
22
|
+
|
|
23
|
+
Metrics/ModuleLength:
|
|
24
|
+
Max: 150
|
|
25
|
+
CountAsOne: ['hash', 'array']
|
|
26
|
+
|
|
27
|
+
Metrics/ClassLength:
|
|
28
|
+
Exclude:
|
|
29
|
+
- 'test/**/*'
|
|
30
|
+
|
|
31
|
+
Metrics/MethodLength:
|
|
32
|
+
Exclude:
|
|
33
|
+
- 'test/**/*'
|
data/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
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.28
|
|
5
|
+
- Added support for batch VAT checks
|
|
6
|
+
- Updated fallback rates for Estonia and Romania
|
|
7
|
+
- [Full Changelog](https://github.com/gem-shards/europe.rb/compare/v0.0.27...v0.0.28)
|
|
4
8
|
## 0.0.27
|
|
5
9
|
- Updated VIES VAT check service to their REST API instead of their SOAP API
|
|
6
10
|
- Improved error handling for the VAT check service
|
data/README.md
CHANGED
|
@@ -7,6 +7,7 @@ This gem provides EU governmental data, extracted from various EU / EC websites.
|
|
|
7
7
|
- [Installation](#installation)
|
|
8
8
|
- [Usage](#usage)
|
|
9
9
|
- [Validating VAT numbers](#validating-vat-numbers)
|
|
10
|
+
- [Batch VAT validation](#batch-vat-validation)
|
|
10
11
|
- [Validate VAT number format](#validate-vat-number-format)
|
|
11
12
|
- [Validate Postal code format](#validate-postal-code-format)
|
|
12
13
|
- [Retrieving VAT rates for each EC/EU member](#retrieving-vat-rates-for-each-eceu-member)
|
|
@@ -55,6 +56,34 @@ Response
|
|
|
55
56
|
:address => nil }
|
|
56
57
|
```
|
|
57
58
|
|
|
59
|
+
### Batch VAT validation
|
|
60
|
+
Validate 3-100 VAT numbers in one request using the VIES batch API. The process is asynchronous: submit numbers, poll for status, then download the report.
|
|
61
|
+
|
|
62
|
+
**Step 1: Submit batch**
|
|
63
|
+
```ruby
|
|
64
|
+
result = Europe::Vat::Batch.validate(['NL009291477B01', 'BE0123456789', 'DE123456789'])
|
|
65
|
+
# => { token: "a1b2c3d4-..." }
|
|
66
|
+
```
|
|
67
|
+
Optionally pass your own VAT number as requester:
|
|
68
|
+
```ruby
|
|
69
|
+
result = Europe::Vat::Batch.validate(vat_numbers, requester: 'NL009291477B01')
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
**Step 2: Check status**
|
|
73
|
+
```ruby
|
|
74
|
+
Europe::Vat::Batch.status(result[:token])
|
|
75
|
+
# => { token: "a1b2c3d4-...", filename: "batch.csv", status: :completed,
|
|
76
|
+
# percentage: 100, created_at: "2026-06-08...", completed_at: "2026-06-08..." }
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**Step 3: Download report**
|
|
80
|
+
```ruby
|
|
81
|
+
report = Europe::Vat::Batch.result(result[:token])
|
|
82
|
+
# => { data: "...csv content...", content_type: "text/csv", filename: "report.csv" }
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
All methods return error symbols on failure (`:timeout`, `:service_unavailable`, `:too_few_rows`, `:too_many_rows`, etc.).
|
|
86
|
+
|
|
58
87
|
### Validate VAT number format
|
|
59
88
|
Call
|
|
60
89
|
```ruby
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'uri'
|
|
4
|
+
require 'net/http'
|
|
5
|
+
require 'json'
|
|
6
|
+
require 'securerandom'
|
|
7
|
+
|
|
8
|
+
module Europe
|
|
9
|
+
module Vat
|
|
10
|
+
# Batch VAT validation via VIES REST API
|
|
11
|
+
module Batch
|
|
12
|
+
BATCH_API_URL = 'https://ec.europa.eu/taxation_customs/vies/rest-api/vat-validation'
|
|
13
|
+
BATCH_REPORT_URL = 'https://ec.europa.eu/taxation_customs/vies/rest-api/vat-validation-report'
|
|
14
|
+
|
|
15
|
+
BATCH_ERRORS = {
|
|
16
|
+
'VOW-ERR-6000' => :invalid_file_extension,
|
|
17
|
+
'VOW-ERR-6001' => :too_many_rows,
|
|
18
|
+
'VOW-ERR-6002' => :too_few_rows,
|
|
19
|
+
'VOW-ERR-6005' => :filter_violation,
|
|
20
|
+
'VOW-ERR-6006' => :token_not_found,
|
|
21
|
+
'VOW-ERR-6007' => :invalid_file_structure,
|
|
22
|
+
'VOW-ERR-6008' => :invalid_file_structure,
|
|
23
|
+
'VOW-ERR-6011' => :file_too_large
|
|
24
|
+
}.freeze
|
|
25
|
+
|
|
26
|
+
BATCH_MIN_ROWS = 3
|
|
27
|
+
BATCH_MAX_ROWS = 100
|
|
28
|
+
|
|
29
|
+
HEADERS = {
|
|
30
|
+
'Content-Type' => 'application/json',
|
|
31
|
+
'Accept' => 'application/json'
|
|
32
|
+
}.freeze
|
|
33
|
+
|
|
34
|
+
class << self
|
|
35
|
+
def validate(vat_numbers, requester: nil)
|
|
36
|
+
return :too_few_rows if vat_numbers.size < BATCH_MIN_ROWS
|
|
37
|
+
return :too_many_rows if vat_numbers.size > BATCH_MAX_ROWS
|
|
38
|
+
|
|
39
|
+
with_error_handling do
|
|
40
|
+
body = parse_json_response(upload(generate_csv(vat_numbers, requester)))
|
|
41
|
+
return body if body.is_a?(Symbol)
|
|
42
|
+
|
|
43
|
+
{ token: body['token'] }
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def status(token)
|
|
48
|
+
with_error_handling do
|
|
49
|
+
body = parse_json_response(http_get("#{BATCH_API_URL}/#{token}", HEADERS))
|
|
50
|
+
return body if body.is_a?(Symbol)
|
|
51
|
+
|
|
52
|
+
{
|
|
53
|
+
token: body['token'], filename: body['filename'],
|
|
54
|
+
status: body['status']&.downcase&.to_sym, percentage: body['percentage'],
|
|
55
|
+
created_at: body['creationDateTime'], completed_at: body['completionTime']
|
|
56
|
+
}
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def result(token)
|
|
61
|
+
with_error_handling do
|
|
62
|
+
response = http_get("#{BATCH_REPORT_URL}/#{token}")
|
|
63
|
+
return handle_error(response) unless response.is_a?(Net::HTTPSuccess)
|
|
64
|
+
|
|
65
|
+
error = check_json_error(response)
|
|
66
|
+
return error if error
|
|
67
|
+
|
|
68
|
+
{ data: response.body, content_type: response['Content-Type'], filename: extract_filename(response) }
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def with_error_handling
|
|
75
|
+
yield
|
|
76
|
+
rescue Net::OpenTimeout, Net::ReadTimeout
|
|
77
|
+
:timeout
|
|
78
|
+
rescue Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::ETIMEDOUT, SocketError, OpenSSL::SSL::SSLError
|
|
79
|
+
:service_unavailable
|
|
80
|
+
rescue JSON::ParserError
|
|
81
|
+
:service_error
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def http_get(url, headers = {})
|
|
85
|
+
uri = URI.parse(url)
|
|
86
|
+
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
|
|
87
|
+
http.request(Net::HTTP::Get.new(uri.request_uri, headers))
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def parse_json_response(response)
|
|
92
|
+
return handle_error(response) unless response.is_a?(Net::HTTPSuccess)
|
|
93
|
+
|
|
94
|
+
body = JSON.parse(response.body)
|
|
95
|
+
return handle_error_body(body) if body['actionSucceed'] == false
|
|
96
|
+
|
|
97
|
+
body
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def check_json_error(response)
|
|
101
|
+
return unless response['Content-Type']&.include?('application/json')
|
|
102
|
+
|
|
103
|
+
body = JSON.parse(response.body)
|
|
104
|
+
handle_error_body(body) if body['actionSucceed'] == false
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def extract_filename(response)
|
|
108
|
+
disposition = response['Content-Disposition']
|
|
109
|
+
return unless disposition
|
|
110
|
+
|
|
111
|
+
disposition.match(/filename="?(.+?)"?(?:;|$)/)&.captures&.first
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def handle_error(response)
|
|
115
|
+
body = JSON.parse(response.body)
|
|
116
|
+
handle_error_body(body)
|
|
117
|
+
rescue JSON::ParserError
|
|
118
|
+
:service_error
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def handle_error_body(body)
|
|
122
|
+
error_code = body.dig('errorWrappers', 0, 'error')
|
|
123
|
+
BATCH_ERRORS[error_code] || Vat::VIES_ERRORS[error_code] || :service_error
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def generate_csv(vat_numbers, requester)
|
|
127
|
+
requester_cc = requester ? requester[0..1] : ''
|
|
128
|
+
requester_vat = requester ? requester[2..-1] : ''
|
|
129
|
+
rows = ['"MS Code","VAT Number","Requester MS Code","Requester VAT Number"']
|
|
130
|
+
vat_numbers.each do |vat|
|
|
131
|
+
rows << "\"#{vat[0..1]}\",\"#{vat[2..-1]}\",\"#{requester_cc}\",\"#{requester_vat}\""
|
|
132
|
+
end
|
|
133
|
+
"#{rows.join("\n")}\n"
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def upload(csv_content)
|
|
137
|
+
uri = URI.parse(BATCH_API_URL)
|
|
138
|
+
boundary = "EuropeGem#{SecureRandom.hex(16)}"
|
|
139
|
+
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
|
|
140
|
+
request = Net::HTTP::Post.new(uri.request_uri)
|
|
141
|
+
request['Content-Type'] = "multipart/form-data; boundary=#{boundary}"
|
|
142
|
+
request['Accept'] = 'application/json'
|
|
143
|
+
request.body = build_multipart_body(csv_content, boundary)
|
|
144
|
+
http.request(request)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def build_multipart_body(csv_content, boundary)
|
|
149
|
+
"--#{boundary}\r\n" \
|
|
150
|
+
"Content-Disposition: form-data; name=\"fileToUpload\"; filename=\"batch.csv\"\r\n" \
|
|
151
|
+
"Content-Type: text/csv\r\n\r\n" \
|
|
152
|
+
"#{csv_content}" \
|
|
153
|
+
"\r\n--#{boundary}--\r\n"
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
data/lib/europe/vat/rates.rb
CHANGED
|
@@ -7,10 +7,10 @@ module Europe
|
|
|
7
7
|
# Rates
|
|
8
8
|
module Rates
|
|
9
9
|
FALLBACK_RATES = {
|
|
10
|
-
AT: 20.0, BE: 21.0, BG: 20.0, CY: 19.0, CZ: 21.0, DE: 19.0, DK: 25.0, EE:
|
|
10
|
+
AT: 20.0, BE: 21.0, BG: 20.0, CY: 19.0, CZ: 21.0, DE: 19.0, DK: 25.0, EE: 24.0,
|
|
11
11
|
EL: 24.0, ES: 21.0, FI: 25.5, FR: 20.0, HR: 25.0, HU: 27.0, IE: 23.0,
|
|
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
|
-
RO:
|
|
13
|
+
RO: 21.0, SE: 25.0, SI: 22.0, SK: 23.0
|
|
14
14
|
}.freeze
|
|
15
15
|
RATES_URL = 'https://raw.githubusercontent.com/benbucksch/eu-vat-rates' \
|
|
16
16
|
'/refs/heads/master/rates.json'
|
data/lib/europe/vat/vat.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'europe/vat/rates'
|
|
4
4
|
require 'europe/vat/format'
|
|
5
|
+
require 'europe/vat/batch'
|
|
5
6
|
require 'uri'
|
|
6
7
|
require 'net/http'
|
|
7
8
|
require 'json'
|
|
@@ -39,6 +40,34 @@ module Europe
|
|
|
39
40
|
:service_unavailable
|
|
40
41
|
end
|
|
41
42
|
|
|
43
|
+
def self.validate_with_country_code(country_code, number)
|
|
44
|
+
return :invalid_input if number.size < 4
|
|
45
|
+
|
|
46
|
+
# Remove country code from number if it's present
|
|
47
|
+
number = number[2..-1] if number[0..1].upcase.to_s == country_code.to_s
|
|
48
|
+
|
|
49
|
+
response = send_request(country_code, number)
|
|
50
|
+
return handle_error_response(response) unless response.is_a?(Net::HTTPSuccess)
|
|
51
|
+
|
|
52
|
+
setup_response(response)
|
|
53
|
+
rescue Net::OpenTimeout, Net::ReadTimeout
|
|
54
|
+
:timeout
|
|
55
|
+
rescue Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::ETIMEDOUT, SocketError, OpenSSL::SSL::SSLError
|
|
56
|
+
:service_unavailable
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def self.batch_validate(vat_numbers, requester: nil)
|
|
60
|
+
Batch.validate(vat_numbers, requester: requester)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def self.batch_status(token)
|
|
64
|
+
Batch.status(token)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def self.batch_result(token)
|
|
68
|
+
Batch.result(token)
|
|
69
|
+
end
|
|
70
|
+
|
|
42
71
|
def self.handle_error_response(response)
|
|
43
72
|
body = JSON.parse(response.body)
|
|
44
73
|
error_code = body.dig('errorWrappers', 0, 'error')
|
data/lib/europe/version.rb
CHANGED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'test_helper'
|
|
4
|
+
|
|
5
|
+
module Europe
|
|
6
|
+
module Vat
|
|
7
|
+
BATCH_SUCCESS_BODY = '{"token":"test-token-uuid"}'
|
|
8
|
+
BATCH_STRUCTURE_ERROR_BODY = '{"actionSucceed":false,"errorWrappers":' \
|
|
9
|
+
'[{"error":"VOW-ERR-6008","message":' \
|
|
10
|
+
'"Your file does not match the expected structure"}]}'
|
|
11
|
+
BATCH_TOKEN_NOT_FOUND_BODY = '{"actionSucceed":false,"errorWrappers":' \
|
|
12
|
+
'[{"error":"VOW-ERR-6006","message":' \
|
|
13
|
+
'"The token is not valid"}]}'
|
|
14
|
+
BATCH_STATUS_PROCESSING_BODY = '{"token":"test-token-uuid","filename":"batch.csv",' \
|
|
15
|
+
'"creationDateTime":"2026-06-08T09:47:13.720Z",' \
|
|
16
|
+
'"status":"PROCESSING","processingStartTime":' \
|
|
17
|
+
'"2026-06-08T09:47:13.852Z","percentage":66.67}'
|
|
18
|
+
BATCH_STATUS_COMPLETED_BODY = '{"token":"test-token-uuid","filename":"batch.csv",' \
|
|
19
|
+
'"creationDateTime":"2026-06-08T09:47:13.720Z",' \
|
|
20
|
+
'"status":"COMPLETED","processingStartTime":' \
|
|
21
|
+
'"2026-06-08T09:47:13.852Z","completionTime":' \
|
|
22
|
+
'"2026-06-08T09:47:49.937Z","percentage":100.0}'
|
|
23
|
+
|
|
24
|
+
class BatchValidationTest < Minitest::Test
|
|
25
|
+
VAT_NUMBERS = %w[NL009291477B01 DE115235681 FR40303265045].freeze
|
|
26
|
+
|
|
27
|
+
def setup
|
|
28
|
+
WebMock.enable!
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def teardown
|
|
32
|
+
WebMock.disable!
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def test_batch_validate_returns_token
|
|
36
|
+
stub_request(:post, Europe::Vat::Batch::BATCH_API_URL)
|
|
37
|
+
.to_return(status: 200, body: BATCH_SUCCESS_BODY,
|
|
38
|
+
headers: { 'Content-Type' => 'application/json' })
|
|
39
|
+
|
|
40
|
+
result = Europe::Vat::Batch.validate(VAT_NUMBERS)
|
|
41
|
+
assert_equal({ token: 'test-token-uuid' }, result)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def test_batch_validate_too_few_rows
|
|
45
|
+
assert_equal :too_few_rows, Europe::Vat::Batch.validate(%w[NL009291477B01 DE115235681])
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def test_batch_validate_too_many_rows
|
|
49
|
+
numbers = (1..101).map { |i| "NL#{i.to_s.rjust(12, '0')}" }
|
|
50
|
+
assert_equal :too_many_rows, Europe::Vat::Batch.validate(numbers)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def test_batch_validate_with_requester
|
|
54
|
+
stub_request(:post, Europe::Vat::Batch::BATCH_API_URL)
|
|
55
|
+
.with { |req| req.body.include?('NL') && req.body.include?('009291477B01') }
|
|
56
|
+
.to_return(status: 200, body: BATCH_SUCCESS_BODY,
|
|
57
|
+
headers: { 'Content-Type' => 'application/json' })
|
|
58
|
+
|
|
59
|
+
result = Europe::Vat::Batch.validate(VAT_NUMBERS, requester: 'NL009291477B01')
|
|
60
|
+
assert_equal({ token: 'test-token-uuid' }, result)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def test_batch_validate_structure_error
|
|
64
|
+
stub_request(:post, Europe::Vat::Batch::BATCH_API_URL)
|
|
65
|
+
.to_return(status: 400, body: BATCH_STRUCTURE_ERROR_BODY,
|
|
66
|
+
headers: { 'Content-Type' => 'application/json' })
|
|
67
|
+
|
|
68
|
+
assert_equal :invalid_file_structure, Europe::Vat::Batch.validate(VAT_NUMBERS)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def test_batch_validate_timeout
|
|
72
|
+
stub_request(:post, Europe::Vat::Batch::BATCH_API_URL).to_timeout
|
|
73
|
+
|
|
74
|
+
assert_equal :timeout, Europe::Vat::Batch.validate(VAT_NUMBERS)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def test_batch_validate_connection_refused
|
|
78
|
+
stub_request(:post, Europe::Vat::Batch::BATCH_API_URL).to_raise(Errno::ECONNREFUSED)
|
|
79
|
+
|
|
80
|
+
assert_equal :service_unavailable, Europe::Vat::Batch.validate(VAT_NUMBERS)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def test_batch_status_processing
|
|
84
|
+
stub_request(:get, "#{Europe::Vat::Batch::BATCH_API_URL}/test-token-uuid")
|
|
85
|
+
.to_return(status: 200, body: BATCH_STATUS_PROCESSING_BODY,
|
|
86
|
+
headers: { 'Content-Type' => 'application/json' })
|
|
87
|
+
|
|
88
|
+
result = Europe::Vat::Batch.status('test-token-uuid')
|
|
89
|
+
assert_equal :processing, result[:status]
|
|
90
|
+
assert_equal 66.67, result[:percentage]
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def test_batch_status_completed
|
|
94
|
+
stub_request(:get, "#{Europe::Vat::Batch::BATCH_API_URL}/test-token-uuid")
|
|
95
|
+
.to_return(status: 200, body: BATCH_STATUS_COMPLETED_BODY,
|
|
96
|
+
headers: { 'Content-Type' => 'application/json' })
|
|
97
|
+
|
|
98
|
+
result = Europe::Vat::Batch.status('test-token-uuid')
|
|
99
|
+
assert_equal :completed, result[:status]
|
|
100
|
+
assert_equal 100.0, result[:percentage]
|
|
101
|
+
assert_equal 'test-token-uuid', result[:token]
|
|
102
|
+
refute_nil result[:completed_at]
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def test_batch_status_invalid_token
|
|
106
|
+
stub_request(:get, "#{Europe::Vat::Batch::BATCH_API_URL}/bad-token")
|
|
107
|
+
.to_return(status: 400, body: BATCH_TOKEN_NOT_FOUND_BODY,
|
|
108
|
+
headers: { 'Content-Type' => 'application/json' })
|
|
109
|
+
|
|
110
|
+
assert_equal :token_not_found, Europe::Vat::Batch.status('bad-token')
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def test_batch_status_timeout
|
|
114
|
+
stub_request(:get, "#{Europe::Vat::Batch::BATCH_API_URL}/test-token-uuid").to_timeout
|
|
115
|
+
|
|
116
|
+
assert_equal :timeout, Europe::Vat::Batch.status('test-token-uuid')
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def test_batch_result_returns_data
|
|
120
|
+
stub_request(:get, "#{Europe::Vat::Batch::BATCH_REPORT_URL}/test-token-uuid")
|
|
121
|
+
.to_return(
|
|
122
|
+
status: 200,
|
|
123
|
+
body: 'fake-xlsx-binary-data',
|
|
124
|
+
headers: {
|
|
125
|
+
'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
126
|
+
'Content-Disposition' => 'attachment; filename="report.xlsx"'
|
|
127
|
+
}
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
result = Europe::Vat::Batch.result('test-token-uuid')
|
|
131
|
+
assert_equal 'fake-xlsx-binary-data', result[:data]
|
|
132
|
+
assert_equal 'report.xlsx', result[:filename]
|
|
133
|
+
assert result[:content_type].include?('spreadsheetml')
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def test_batch_result_invalid_token
|
|
137
|
+
stub_request(:get, "#{Europe::Vat::Batch::BATCH_REPORT_URL}/bad-token")
|
|
138
|
+
.to_return(status: 400, body: BATCH_TOKEN_NOT_FOUND_BODY,
|
|
139
|
+
headers: { 'Content-Type' => 'application/json' })
|
|
140
|
+
|
|
141
|
+
assert_equal :token_not_found, Europe::Vat::Batch.result('bad-token')
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def test_batch_result_timeout
|
|
145
|
+
stub_request(:get, "#{Europe::Vat::Batch::BATCH_REPORT_URL}/test-token-uuid").to_timeout
|
|
146
|
+
|
|
147
|
+
assert_equal :timeout, Europe::Vat::Batch.result('test-token-uuid')
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def test_generate_batch_csv_format
|
|
151
|
+
csv = Europe::Vat::Batch.send(:generate_csv, VAT_NUMBERS, nil)
|
|
152
|
+
lines = csv.strip.split("\n")
|
|
153
|
+
|
|
154
|
+
assert_equal 4, lines.size
|
|
155
|
+
assert_equal '"MS Code","VAT Number","Requester MS Code","Requester VAT Number"', lines[0]
|
|
156
|
+
assert_equal '"NL","009291477B01","",""', lines[1]
|
|
157
|
+
assert_equal '"DE","115235681","",""', lines[2]
|
|
158
|
+
assert_equal '"FR","40303265045","",""', lines[3]
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def test_generate_batch_csv_with_requester
|
|
162
|
+
csv = Europe::Vat::Batch.send(:generate_csv, VAT_NUMBERS, 'NL009291477B01')
|
|
163
|
+
lines = csv.strip.split("\n")
|
|
164
|
+
|
|
165
|
+
assert_equal '"NL","009291477B01","NL","009291477B01"', lines[1]
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Delegation tests — ensure Europe::Vat.batch_* still works
|
|
169
|
+
def test_delegation_batch_validate
|
|
170
|
+
stub_request(:post, Europe::Vat::Batch::BATCH_API_URL)
|
|
171
|
+
.to_return(status: 200, body: BATCH_SUCCESS_BODY,
|
|
172
|
+
headers: { 'Content-Type' => 'application/json' })
|
|
173
|
+
|
|
174
|
+
result = Europe::Vat.batch_validate(VAT_NUMBERS)
|
|
175
|
+
assert_equal({ token: 'test-token-uuid' }, result)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def test_delegation_batch_status
|
|
179
|
+
stub_request(:get, "#{Europe::Vat::Batch::BATCH_API_URL}/test-token-uuid")
|
|
180
|
+
.to_return(status: 200, body: BATCH_STATUS_COMPLETED_BODY,
|
|
181
|
+
headers: { 'Content-Type' => 'application/json' })
|
|
182
|
+
|
|
183
|
+
result = Europe::Vat.batch_status('test-token-uuid')
|
|
184
|
+
assert_equal :completed, result[:status]
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def test_delegation_batch_result
|
|
188
|
+
stub_request(:get, "#{Europe::Vat::Batch::BATCH_REPORT_URL}/test-token-uuid")
|
|
189
|
+
.to_return(
|
|
190
|
+
status: 200,
|
|
191
|
+
body: 'fake-xlsx-binary-data',
|
|
192
|
+
headers: {
|
|
193
|
+
'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
194
|
+
'Content-Disposition' => 'attachment; filename="report.xlsx"'
|
|
195
|
+
}
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
result = Europe::Vat.batch_result('test-token-uuid')
|
|
199
|
+
assert_equal 'fake-xlsx-binary-data', result[:data]
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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.28
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Gem shards
|
|
@@ -165,6 +165,7 @@ files:
|
|
|
165
165
|
- lib/europe/postal/postal.rb
|
|
166
166
|
- lib/europe/rake_task.rb
|
|
167
167
|
- lib/europe/tasks/eurostat.rake
|
|
168
|
+
- lib/europe/vat/batch.rb
|
|
168
169
|
- lib/europe/vat/format.rb
|
|
169
170
|
- lib/europe/vat/rates.rb
|
|
170
171
|
- lib/europe/vat/vat.rb
|
|
@@ -174,6 +175,7 @@ files:
|
|
|
174
175
|
- test/europe/currency/exchange_rates_test.rb
|
|
175
176
|
- test/europe/eurostat/eurostat_test.rb
|
|
176
177
|
- test/europe/postal/format_test.rb
|
|
178
|
+
- test/europe/vat/batch_validation_test.rb
|
|
177
179
|
- test/europe/vat/format_test.rb
|
|
178
180
|
- test/europe/vat/rates_test.rb
|
|
179
181
|
- test/europe/vat/validation_test.rb
|
|
@@ -209,6 +211,7 @@ test_files:
|
|
|
209
211
|
- test/europe/currency/exchange_rates_test.rb
|
|
210
212
|
- test/europe/eurostat/eurostat_test.rb
|
|
211
213
|
- test/europe/postal/format_test.rb
|
|
214
|
+
- test/europe/vat/batch_validation_test.rb
|
|
212
215
|
- test/europe/vat/format_test.rb
|
|
213
216
|
- test/europe/vat/rates_test.rb
|
|
214
217
|
- test/europe/vat/validation_test.rb
|