valvat 1.1.4 → 1.2.0

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.
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