valvat 1.1.4 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+ require 'net/http'
5
+ require 'erb'
6
+ require 'rexml'
7
+
8
+ class Valvat
9
+ class Lookup
10
+ class VIES < Base
11
+ ENDPOINT_URI = URI('https://ec.europa.eu/taxation_customs/vies/services/checkVatService').freeze
12
+ HEADERS = {
13
+ 'Accept' => 'text/xml;charset=UTF-8',
14
+ 'Content-Type' => 'text/xml;charset=UTF-8',
15
+ 'SOAPAction' => ''
16
+ }.freeze
17
+
18
+ BODY = <<-XML.gsub(/^\s+/, '')
19
+ <soapenv:Envelope
20
+ xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
21
+ xmlns:urn="urn:ec.europa.eu:taxud:vies:services:checkVat:types">
22
+ <soapenv:Header/>
23
+ <soapenv:Body>
24
+ <urn:checkVat<%= 'Approx' if @requester %>>
25
+ <urn:countryCode><%= @vat.vat_country_code %></urn:countryCode>
26
+ <urn:vatNumber><%= @vat.to_s_wo_country %></urn:vatNumber>
27
+ <% if @requester %>
28
+ <urn:requesterCountryCode><%= @vat.vat_country_code %></urn:requesterCountryCode>
29
+ <urn:requesterVatNumber><%= @vat.to_s_wo_country %></urn:requesterVatNumber>
30
+ <% end %>
31
+ </urn:checkVat<%= 'Approx' if @requester %>>
32
+ </soapenv:Body>
33
+ </soapenv:Envelope>
34
+ XML
35
+ BODY_TEMPLATE = ERB.new(BODY).freeze
36
+
37
+ private
38
+
39
+ def endpoint_uri
40
+ ENDPOINT_URI
41
+ end
42
+
43
+ def build_request(uri)
44
+ request = Net::HTTP::Post.new(uri.request_uri, HEADERS)
45
+ request.body = BODY_TEMPLATE.result(binding)
46
+ request
47
+ end
48
+
49
+ def parse(body)
50
+ doc = REXML::Document.new(body)
51
+ elements = doc.get_elements('/env:Envelope/env:Body').first.first
52
+ convert_values(elements.each_with_object({}) do |el, hash|
53
+ hash[convert_key(el.name)] = convert_value(el.text)
54
+ end)
55
+ end
56
+
57
+ def convert_key(key)
58
+ key.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
59
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
60
+ .tr('-', '_')
61
+ .sub(/\Atrader_/, '')
62
+ .downcase.to_sym
63
+ end
64
+
65
+ def convert_value(value)
66
+ value == '---' ? nil : value
67
+ end
68
+
69
+ def convert_values(hash)
70
+ return build_fault(hash) if hash[:faultstring]
71
+
72
+ hash[:valid] = hash[:valid] == 'true' || (hash[:valid] == 'false' ? false : nil) if hash.key?(:valid)
73
+ hash[:request_date] = Date.parse(hash[:request_date]) if hash.key?(:request_date)
74
+ hash
75
+ end
76
+
77
+ FAULTS = {
78
+ 'SERVICE_UNAVAILABLE' => ServiceUnavailable,
79
+ 'MS_UNAVAILABLE' => MemberStateUnavailable,
80
+ 'INVALID_REQUESTER_INFO' => InvalidRequester,
81
+ 'TIMEOUT' => Timeout,
82
+ 'VAT_BLOCKED' => BlockedError,
83
+ 'IP_BLOCKED' => BlockedError,
84
+ 'GLOBAL_MAX_CONCURRENT_REQ' => RateLimitError,
85
+ 'GLOBAL_MAX_CONCURRENT_REQ_TIME' => RateLimitError,
86
+ 'MS_MAX_CONCURRENT_REQ' => RateLimitError,
87
+ 'MS_MAX_CONCURRENT_REQ_TIME' => RateLimitError
88
+ }.freeze
89
+
90
+ def build_fault(hash)
91
+ fault = hash[:faultstring]
92
+ return hash.merge({ valid: false }) if fault == 'INVALID_INPUT'
93
+
94
+ hash.merge({ error: (FAULTS[fault] || UnknownLookupError).new(fault, self.class) })
95
+ end
96
+ end
97
+ end
98
+ end
data/lib/valvat/lookup.rb CHANGED
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'lookup/vies'
4
+ require_relative 'lookup/hmrc'
5
+
3
6
  class Valvat
4
7
  class Lookup
5
8
  def initialize(vat, options = {})
@@ -10,9 +13,9 @@ class Valvat
10
13
 
11
14
  def validate
12
15
  return false if !@options[:skip_local_validation] && !@vat.valid?
13
- return handle_vies_error(response[:error]) if response[:error]
16
+ return handle_error(response[:error]) if response[:error]
14
17
 
15
- response[:valid] && show_details? ? response.to_hash : response[:valid]
18
+ response[:valid] && show_details? ? response : response[:valid]
16
19
  end
17
20
 
18
21
  class << self
@@ -28,15 +31,23 @@ class Valvat
28
31
  end
29
32
 
30
33
  def response
31
- @response ||= Request.new(@vat, @options).perform
34
+ @response ||= webservice.new(@vat, @options).perform
35
+ end
36
+
37
+ def webservice
38
+ case @vat.vat_country_code
39
+ when 'GB' then HMRC
40
+ else
41
+ VIES
42
+ end
32
43
  end
33
44
 
34
45
  def show_details?
35
46
  @options[:requester] || @options[:detail]
36
47
  end
37
48
 
38
- def handle_vies_error(error)
39
- if error.is_a?(ViesMaintenanceError)
49
+ def handle_error(error)
50
+ if error.is_a?(MaintenanceError)
40
51
  raise error if @options[:raise_error]
41
52
  else
42
53
  raise error unless @options[:raise_error] == false
data/lib/valvat/utils.rb CHANGED
@@ -6,7 +6,6 @@ class Valvat
6
6
  module Utils
7
7
  EU_MEMBER_STATES = %w[AT BE BG CY CZ DE DK EE ES FI FR GR HR HU IE IT LT LU LV MT NL PL PT RO SE SI SK].freeze
8
8
  SUPPORTED_STATES = EU_MEMBER_STATES + %w[GB]
9
- EU_COUNTRIES = EU_MEMBER_STATES # TODO: Remove constant
10
9
  COUNTRY_PATTERN = /\A([A-Z]{2})(.+)\Z/.freeze
11
10
  NORMALIZE_PATTERN = /([[:punct:][:cntrl:]]|[[:space:]])+/.freeze
12
11
  CONVERT_VAT_TO_ISO_COUNTRY = { 'EL' => 'GR', 'XI' => 'GB' }.freeze
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Valvat
4
- VERSION = '1.1.4'
4
+ VERSION = '1.2.0'
5
5
  end
data/lib/valvat.rb CHANGED
@@ -1,16 +1,62 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'valvat/error'
4
- require 'valvat/local'
4
+ require 'valvat/utils'
5
+ require 'valvat/syntax'
6
+ require 'valvat/checksum'
7
+ require 'valvat/version'
5
8
  require 'valvat/lookup'
6
- require 'valvat/lookup/request'
7
- require 'valvat/lookup/response'
8
- require 'valvat/lookup/fault'
9
9
  require 'active_model/validations/valvat_validator' if defined?(ActiveModel)
10
10
 
11
11
  class Valvat
12
+ def initialize(raw)
13
+ @raw = Valvat::Utils.normalize(raw || '')
14
+ @vat_country_code, @to_s_wo_country = to_a
15
+ end
16
+
17
+ attr_reader :raw, :vat_country_code, :to_s_wo_country
18
+
19
+ def blank?
20
+ raw.nil? || raw.strip == ''
21
+ end
22
+
23
+ def valid?
24
+ Valvat::Syntax.validate(self)
25
+ end
26
+
27
+ def valid_checksum?
28
+ Valvat::Checksum.validate(self)
29
+ end
30
+
12
31
  def exists?(options = {})
13
32
  Valvat::Lookup.validate(self, options)
14
33
  end
15
34
  alias exist? exists?
35
+
36
+ def iso_country_code
37
+ Valvat::Utils.vat_country_to_iso_country(vat_country_code)
38
+ end
39
+
40
+ # TODO: Remove method / not in use
41
+ def european?
42
+ puts 'DEPRECATED: #european? is deprecated. Instead access Valvat::Utils::EU_MEMBER_STATES directly.'
43
+
44
+ Valvat::Utils::EU_MEMBER_STATES.include?(iso_country_code)
45
+ end
46
+
47
+ def to_a
48
+ Valvat::Utils.split(raw)
49
+ end
50
+
51
+ def to_s
52
+ raw
53
+ end
54
+
55
+ def inspect
56
+ "#<Valvat #{[raw, iso_country_code].compact.join(' ')}>"
57
+ end
58
+ end
59
+
60
+ def Valvat(vat) # rubocop:disable Naming/MethodName
61
+ vat.is_a?(Valvat) ? vat : Valvat.new(vat)
16
62
  end
data/spec/spec_helper.rb CHANGED
@@ -8,12 +8,13 @@ rescue LoadError
8
8
  end
9
9
 
10
10
  require "#{File.dirname(__FILE__)}/../lib/valvat.rb"
11
+ require 'webmock/rspec'
12
+ WebMock.allow_net_connect!
11
13
 
12
14
  RSpec.configure do |config|
13
15
  config.mock_with :rspec
14
16
  config.filter_run focus: true
15
17
  config.run_all_when_everything_filtered = true
16
- config.backtrace_exclusion_patterns = [%r{rspec/(core|expectations)}]
17
18
  end
18
19
 
19
20
  I18n.enforce_available_locales = false if defined?(I18n)
@@ -4,7 +4,7 @@ require 'spec_helper'
4
4
 
5
5
  describe Valvat::Checksum::ES do
6
6
  %w[ESA13585625 ESB83871236 ESE54507058 ES25139013J ESQ1518001A ESQ5018001G ESX4942978W ESX7676464F ESB10317980
7
- ESY3860557K ESY2207765D].each do |valid_vat|
7
+ ESY3860557K ESY2207765D ES28350472M ES41961720Z ESM1171170X ESK0928769Y].each do |valid_vat|
8
8
  it "returns true on valid VAT #{valid_vat}" do
9
9
  expect(Valvat::Checksum.validate(valid_vat)).to be(true)
10
10
  end
@@ -15,4 +15,51 @@ describe Valvat::Checksum::ES do
15
15
  expect(Valvat::Checksum.validate(invalid_vat)).to be(false)
16
16
  end
17
17
  end
18
+
19
+ describe 'if starts with [KLMXYZ\\d], is always a natural person' do
20
+ invalid_vat = 'ESX65474207'
21
+ it "returns false on invalid VAT #{invalid_vat}" do
22
+ expect(Valvat::Checksum.validate(invalid_vat)).to be(false)
23
+ end
24
+ end
25
+
26
+ describe 'some CIF categories (NPQRSW) require control-digit to be a letter' do
27
+ invalid_vat = 'ESP65474207'
28
+ valid_vat = 'ESP6547420G'
29
+
30
+ it "returns false on invalid VAT #{invalid_vat}" do
31
+ expect(Valvat::Checksum.validate(invalid_vat)).to be(false)
32
+ end
33
+
34
+ it "returns true on valid VAT #{valid_vat}" do
35
+ expect(Valvat::Checksum.validate(valid_vat)).to be(true)
36
+ end
37
+ end
38
+
39
+ describe 'some CIF categories (CDFGJNUV) allow both a numeric check digit and a letter' do
40
+ %w[ESC65474207 ESC6547420G].each do |valid_vat|
41
+ it "returns true on valid VAT #{valid_vat}" do
42
+ expect(Valvat::Checksum.validate(valid_vat)).to be(true)
43
+ end
44
+ end
45
+ end
46
+
47
+ describe 'applies special rules to validation' do
48
+ describe 'special NIF categories (KLM) require CD to be a letter and first two digits '\
49
+ 'to be between 01 and 56 (inclusive)' do
50
+ %w[ESK8201230M ESK0001230B].each do |invalid_vat|
51
+ it "returns false on invalid VAT #{invalid_vat}" do
52
+ expect(Valvat::Checksum.validate(invalid_vat)).to be(false)
53
+ end
54
+ end
55
+ end
56
+
57
+ describe 'arbitrarily invalid VATs' do
58
+ %w[ESX0000000T ES00000001R ES00000000T ES99999999R].each do |invalid_vat|
59
+ it "returns false on invalid VAT #{invalid_vat}" do
60
+ expect(Valvat::Checksum.validate(invalid_vat)).to be(false)
61
+ end
62
+ end
63
+ end
64
+ end
18
65
  end
@@ -21,6 +21,7 @@ describe Valvat::Checksum::GB do
21
21
 
22
22
  it 'is true for a new format valid vat' do
23
23
  expect(Valvat::Checksum.validate('GB434031439')).to be true
24
+ expect(Valvat::Checksum.validate('GB727255821')).to be true
24
25
  end
25
26
 
26
27
  it 'is false for an old format VAT in forbidden group' do
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Valvat::Lookup::HMRC do
6
+ before do
7
+ stub_const('Valvat::Lookup::HMRC::ENDPOINT_URL', 'https://test-api.service.hmrc.gov.uk/organisations/vat/check-vat-number/lookup')
8
+ end
9
+
10
+ it 'returns hash with valid: true on success' do
11
+ response = described_class.new('GB553557881', { uk: true }).perform
12
+
13
+ expect(response).to match({
14
+ valid: true,
15
+ address: "131B Barton Hamlet\nSW97 5CK\nGB",
16
+ country_code: 'GB',
17
+ vat_number: '553557881',
18
+ name: 'Credite Sberger Donal Inc.',
19
+ request_date: kind_of(Time)
20
+ })
21
+ end
22
+
23
+ it 'returns hash with valid: false on invalid input' do
24
+ response = described_class.new('GB123456789', { uk: true }).perform
25
+ expect(response).to match({ valid: false })
26
+ end
27
+
28
+ it 'returns hash with valid: false on valid input with :uk option not set' do
29
+ response = described_class.new('GB553557881', {}).perform
30
+ expect(response).to match({ valid: false })
31
+ end
32
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Valvat::Lookup::VIES do
6
+ it 'returns hash with valid: true on success' do
7
+ response = described_class.new('IE6388047V', {}).perform
8
+
9
+ expect(response).to match({
10
+ valid: true,
11
+ address: '3RD FLOOR, GORDON HOUSE, BARROW STREET, DUBLIN 4',
12
+ country_code: 'IE',
13
+ vat_number: '6388047V',
14
+ name: 'GOOGLE IRELAND LIMITED',
15
+ request_date: kind_of(Date)
16
+ })
17
+ end
18
+
19
+ it 'returns hash with valid: false on invalid input' do
20
+ response = described_class.new('XC123123', {}).perform
21
+ expect(response.to_hash).to match({ valid: false, faultstring: 'INVALID_INPUT', faultcode: 'env:Server' })
22
+ end
23
+ end