8 | 8 |
# Original author: #
|
9 | 9 |
# IT Development software #
|
10 | 10 |
# European VAT number validator Version 1.0.2 #
|
11 | |
# Created 06/08/2003 Last Modified 30/11/2012 #
|
|
11 |
# Created 06/08/2003 #
|
12 | 12 |
# #
|
13 | 13 |
# Maintainership kindly handed over to David Precious (BIGPRESH) in 2015 #
|
14 | 14 |
############################################################################
|
|
16 | 16 |
# Copyright 2003 Bernard Nauwelaerts All Rights Reserved. #
|
17 | 17 |
# Copyright 2015 David Precious All Rights Reserved. #
|
18 | 18 |
# #
|
19 | |
# THIS SOFTWARE IS RELEASED UNDER THE GNU Public Licence #
|
|
19 |
# THIS SOFTWARE IS RELEASED UNDER THE GNU Public Licence version 3 #
|
20 | 20 |
# Please see COPYING for details #
|
21 | 21 |
# #
|
22 | 22 |
# DISCLAIMER #
|
|
26 | 26 |
# #
|
27 | 27 |
############################################################################
|
28 | 28 |
use strict;
|
29 | |
|
30 | |
BEGIN {
|
31 | |
$Business::Tax::VAT::Validation::VERSION = '1.12';
|
32 | |
use HTTP::Request::Common qw(POST);
|
33 | |
use LWP::UserAgent;
|
34 | |
}
|
|
29 |
use warnings;
|
|
30 |
|
|
31 |
our $VERSION = '1.20';
|
|
32 |
|
|
33 |
use HTTP::Request::Common qw(POST);
|
|
34 |
use LWP::UserAgent;
|
|
35 |
use JSON qw/ decode_json /;
|
35 | 36 |
|
36 | 37 |
=head1 NAME
|
37 | 38 |
|
38 | |
Business::Tax::VAT::Validation - Validate EU VAT numbers against VIES
|
|
39 |
Business::Tax::VAT::Validation - Validate EU VAT numbers against VIES/HMRC
|
39 | 40 |
|
40 | 41 |
=head1 SYNOPSIS
|
41 | 42 |
|
|
60 | 61 |
state are performed first, to avoid unnecessarily sending queries to VIES for
|
61 | 62 |
input that could never be valid.
|
62 | 63 |
|
|
64 |
It also supports looking up VAT codes from the United Kingdom by using the
|
|
65 |
REST API provided by their HMRC.
|
63 | 66 |
|
64 | 67 |
=head1 CONSTRUCTOR
|
65 | 68 |
|
|
82 | 85 |
my ( $class, %arg ) = @_;
|
83 | 86 |
my $self = {
|
84 | 87 |
baseurl => $arg{baseurl} || 'https://ec.europa.eu/taxation_customs/vies/services/checkVatService',
|
|
88 |
hmrc_baseurl => $arg{hmrc_baseurl} || 'https://api.service.hmrc.gov.uk/organisations/vat/check-vat-number/lookup/',
|
85 | 89 |
error => '',
|
86 | 90 |
error_code => 0,
|
87 | 91 |
response => '',
|
|
115 | 119 |
SE => '[0-9]{12}',
|
116 | 120 |
SI => '[0-9]{8}',
|
117 | 121 |
SK => '[0-9]{10}',
|
|
122 |
XI => '([0-9]{3} ?[0-9]{4} ?[0-9]{2}|[0-9]{3} ?[0-9]{4} ?[0-9]{2} ?[0-9]{3}|GD[0-9]{3}|HA[0-9]{3})',
|
118 | 123 |
},
|
119 | 124 |
proxy => $arg{-proxy},
|
120 | |
informations => {}
|
|
125 |
information => {}
|
121 | 126 |
};
|
122 | 127 |
$self = bless $self, $class;
|
123 | 128 |
$self->{members} = join( '|', keys %{ $self->{re} } );
|
|
130 | 135 |
|
131 | 136 |
=over 4
|
132 | 137 |
|
133 | |
=item B<member_states> Returns all member states 2-digit codes as array
|
|
138 |
=item B<member_states> Returns all supported country codes.
|
|
139 |
|
|
140 |
These are ISO 3166-1 alpha-2 country codes with two exceptions. This module
|
|
141 |
supports VAT codes from all current European Union member states and The United
|
|
142 |
Kingdom of Great Britain and Northern Ireland.
|
|
143 |
|
|
144 |
=over 4
|
|
145 |
|
|
146 |
=item C<EL> Greece
|
|
147 |
|
|
148 |
Must be used in place of Greece's proper code.
|
|
149 |
|
|
150 |
=item C<XI> Northern Ireland
|
|
151 |
|
|
152 |
May be used rather than C<GB> for checking a Northern Irish company.
|
|
153 |
|
|
154 |
=back
|
134 | 155 |
|
135 | 156 |
@ms=$hvatn->member_states;
|
136 | 157 |
|
|
178 | 199 |
Valid MS values are :
|
179 | 200 |
|
180 | 201 |
AT, BE, BG, CY, CZ, DE, DK, EE, EL, ES,
|
181 | |
FI, FR, GB, HU, IE, IT, LU, LT, LV, MT,
|
182 | |
NL, PL, PT, RO, SE, SI, SK
|
|
202 |
FI, FR, GB, HR, HU, IE, IT, LU, LT, LV,
|
|
203 |
MT, NL, PL, PT, RO, SE, SI, SK, XI
|
183 | 204 |
|
184 | 205 |
=cut
|
185 | 206 |
|
186 | 207 |
sub check {
|
187 | 208 |
my ($self, $vatNumber, $countryCode, @other) = @_; # @other is here for backward compatibility purposes
|
|
209 |
$self->{information} = {};
|
188 | 210 |
return $self->_set_error('You must provide a VAT number') unless $vatNumber;
|
189 | 211 |
$countryCode ||= '';
|
190 | 212 |
( $vatNumber, $countryCode ) = $self->_format_vatn( $vatNumber, $countryCode );
|
191 | 213 |
if ($vatNumber) {
|
192 | |
my $ua = LWP::UserAgent->new;
|
193 | |
if ( ref $self->{proxy} eq 'ARRAY' ) {
|
194 | |
$ua->proxy( @{ $self->{proxy} } );
|
195 | |
} else {
|
196 | |
$ua->env_proxy;
|
|
214 |
if ($countryCode eq 'GB') {
|
|
215 |
return $self->_check_hmrc($vatNumber, $countryCode);
|
197 | 216 |
}
|
198 | |
$ua->agent( 'Business::Tax::VAT::Validation/'. $Business::Tax::VAT::Validation::VERSION );
|
199 | |
|
200 | |
my $request = HTTP::Request->new(POST => $self->{baseurl});
|
201 | |
$request->header(SOAPAction => 'http://www.w3.org/2003/05/soap-envelope');
|
202 | |
$request->content(_in_soap_envelope($vatNumber, $countryCode));
|
203 | |
$request->content_type("Content-Type: application/soap+xml; charset=utf-8");
|
204 | |
|
205 | |
my $response = $ua->request($request);
|
206 | |
|
207 | |
return $countryCode . '-' . $vatNumber if $self->_is_res_ok( $response->code, $response->decoded_content );
|
|
217 |
return $self->_check_vies($vatNumber, $countryCode);
|
208 | 218 |
}
|
209 | 219 |
0;
|
210 | 220 |
}
|
|
219 | 229 |
|
220 | 230 |
sub local_check {
|
221 | 231 |
my ( $self, $vatn, $mscc, @other ) = @_; # @other is here for backward compatibility purposes
|
|
232 |
$self->{information} = {};
|
222 | 233 |
return $self->_set_error('You must provide a VAT number') unless $vatn;
|
223 | 234 |
$mscc ||= '';
|
224 | 235 |
( $vatn, $mscc ) = $self->_format_vatn( $vatn, $mscc );
|
|
230 | 241 |
}
|
231 | 242 |
}
|
232 | 243 |
|
233 | |
=item B<informations> - Returns informations related to the last validated VAT number
|
234 | |
|
235 | |
%infos=$hvatn->informations();
|
236 | |
|
237 | |
|
238 | |
=cut
|
239 | |
|
240 | |
sub informations {
|
|
244 |
=item B<information> - Returns information related to the last checked VAT number
|
|
245 |
|
|
246 |
# Get all available information as a hashref:
|
|
247 |
my $info = $hvatn->information();
|
|
248 |
|
|
249 |
# Get a particular key:
|
|
250 |
my $address = $hvatn->information('address');
|
|
251 |
|
|
252 |
Which information is offered depends on the checker used - for UK VAT numbers,
|
|
253 |
checked via the HMRC API, C<address> is the only key which will be set.
|
|
254 |
|
|
255 |
For EU VAT numbers checked via VIES, you can expect C<name> and C<address>.
|
|
256 |
This hashref will be reset every time you call check() or local_check()
|
|
257 |
|
|
258 |
=cut
|
|
259 |
|
|
260 |
sub information {
|
241 | 261 |
my ( $self, $key, @other ) = @_;
|
242 | 262 |
if ($key) {
|
243 | |
return $self->{informations}{$key}
|
|
263 |
return $self->{information}{$key}
|
244 | 264 |
} else {
|
245 | |
return ($self->{informations})
|
|
265 |
return ($self->{information})
|
246 | 266 |
}
|
247 | 267 |
}
|
248 | 268 |
|
|
319 | 339 |
}
|
320 | 340 |
|
321 | 341 |
### PRIVATE FUNCTIONS ==========================================================
|
|
342 |
sub _get_ua {
|
|
343 |
my ($self) = @_;
|
|
344 |
my $ua = LWP::UserAgent->new;
|
|
345 |
if ( ref $self->{proxy} eq 'ARRAY' ) {
|
|
346 |
$ua->proxy( @{ $self->{proxy} } );
|
|
347 |
} else {
|
|
348 |
$ua->env_proxy;
|
|
349 |
}
|
|
350 |
$ua->agent( 'Business::Tax::VAT::Validation/'. $Business::Tax::VAT::Validation::VERSION );
|
|
351 |
return $ua;
|
|
352 |
}
|
|
353 |
|
|
354 |
sub _check_vies {
|
|
355 |
my ($self, $vatNumber, $countryCode) = @_;
|
|
356 |
my $ua = $self->_get_ua();
|
|
357 |
my $request = HTTP::Request->new(POST => $self->{baseurl});
|
|
358 |
$request->header(SOAPAction => 'http://www.w3.org/2003/05/soap-envelope');
|
|
359 |
$request->content(_in_soap_envelope($vatNumber, $countryCode));
|
|
360 |
$request->content_type("Content-Type: application/soap+xml; charset=utf-8");
|
|
361 |
|
|
362 |
my $response = $ua->request($request);
|
|
363 |
|
|
364 |
return $countryCode . '-' . $vatNumber if $self->_is_res_ok( $response->code, $response->decoded_content );
|
|
365 |
}
|
|
366 |
|
|
367 |
sub _check_hmrc {
|
|
368 |
my ($self, $vatNumber, $countryCode) = @_;
|
|
369 |
my $ua = $self->_get_ua();
|
|
370 |
|
|
371 |
my $request = HTTP::Request->new(GET => $self->{hmrc_baseurl}.$vatNumber);
|
|
372 |
$request->header(Accept => 'application/vnd.hmrc.1.0+json');
|
|
373 |
my $response = $ua->request($request);
|
|
374 |
|
|
375 |
$self->{res} = $response->decoded_content;
|
|
376 |
if ($response->code == 200) {
|
|
377 |
my $data = decode_json($self->{res});
|
|
378 |
$self->{information}->{name} = $data->{target}->{name};
|
|
379 |
my $line = 1;
|
|
380 |
my $address = "";
|
|
381 |
while (defined $data->{target}->{address}->{"line$line"}) {
|
|
382 |
$address .= $data->{target}->{address}->{"line$line"}."\n";
|
|
383 |
$line++;
|
|
384 |
}
|
|
385 |
$address .= $data->{target}->{address}->{postcode};
|
|
386 |
$address .= "\n".$data->{target}->{address}->{countryCode};
|
|
387 |
$self->{information}->{address} = $address;
|
|
388 |
$self->_set_error( -1, 'Valid VAT Number');
|
|
389 |
}
|
|
390 |
elsif ($response->code == 404) {
|
|
391 |
return $self->_set_error( 2, 'Invalid VAT Number ('.$vatNumber.')');
|
|
392 |
}
|
|
393 |
elsif ($response->code == 400) {
|
|
394 |
return $self->_set_error( 3, 'VAT number badly formed ('.$vatNumber.')');
|
|
395 |
}
|
|
396 |
else {
|
|
397 |
return $self->_set_error( 500, 'Could not contact HMRC: '.$response->status_line);
|
|
398 |
}
|
|
399 |
|
|
400 |
return $countryCode . '-' . $vatNumber;
|
|
401 |
}
|
|
402 |
|
322 | 403 |
sub _format_vatn {
|
323 | 404 |
my ( $self, $vatn, $mscc ) = @_;
|
324 | 405 |
my $null = '';
|
|
356 | 437 |
|
357 | 438 |
sub _is_res_ok {
|
358 | 439 |
my ( $self, $code, $res ) = @_;
|
359 | |
$self->{informations}={};
|
|
440 |
$self->{information}={};
|
360 | 441 |
$res=~s/[\r\n]/ /g;
|
361 | 442 |
$self->{response} = $res;
|
362 | 443 |
if ($code == 200) {
|
|
364 | 445 |
my $v = $1;
|
365 | 446 |
if ($v eq 'true' || $v eq '1') {
|
366 | 447 |
if ($res=~m/<name> *(.*?) *<\/name>/) {
|
367 | |
$self->{informations}{name} = $1
|
|
448 |
$self->{information}{name} = $1
|
368 | 449 |
}
|
369 | 450 |
if ($res=~m/<address> *(.*?) *<\/address>/) {
|
370 | |
$self->{informations}{address} = $1
|
|
451 |
$self->{information}{address} = $1
|
371 | 452 |
}
|
372 | 453 |
$self->_set_error( -1, 'Valid VAT Number');
|
373 | 454 |
return 1;
|
|
411 | 492 |
|
412 | 493 |
LWP::UserAgent
|
413 | 494 |
|
414 | |
I<http://ec.europa.eu/taxation_customs/vies/faqvies.do> for the FAQs related to the VIES service.
|
415 | |
|
|
495 |
L<http://ec.europa.eu/taxation_customs/vies/faqvies.do> for the FAQs related to the VIES service.
|
|
496 |
|
|
497 |
L<https://developer.service.hmrc.gov.uk/api-documentation/docs/api/service/vat-registered-companies-api/1.0>
|
|
498 |
for details of the service provided by the UK's HMRC.
|
416 | 499 |
|
417 | 500 |
=head1 FEEDBACK
|
418 | 501 |
|
|
445 | 528 |
Martin H. Sluka, noris network AG, Germany.
|
446 | 529 |
|
447 | 530 |
=item *
|
448 | |
Simon Williams, UK2 Limited, United Kingdom & Benoît Galy, Greenacres, France & Raluca Boboia, Evozon, Romania
|
|
531 |
Simon Williams, UK2 Limited, United Kingdom
|
|
532 |
|
|
533 |
=item *
|
|
534 |
Benoît Galy, Greenacres, France
|
|
535 |
|
|
536 |
=item *
|
|
537 |
Raluca Boboia, Evozon, Romania
|
449 | 538 |
|
450 | 539 |
=item *
|
451 | 540 |
Dave O., POBox, U.S.A.
|
|
465 | 554 |
=item *
|
466 | 555 |
Torsten Mueller, Archesoft, Germany
|
467 | 556 |
|
|
557 |
=item *
|
|
558 |
Dave Lambley (davel), GoDaddy, United Kingdom
|
|
559 |
|
468 | 560 |
=back
|
469 | 561 |
|
470 | 562 |
=head1 LICENSE
|
471 | 563 |
|
472 | |
GPL. Enjoy! See COPYING for further information on the GPL.
|
|
564 |
GPL3. Enjoy! See COPYING for further information on the GPL.
|
473 | 565 |
|
474 | 566 |
|
475 | 567 |
=head1 DISCLAIMER
|
476 | 568 |
|
477 | |
See I<http://ec.europa.eu/taxation_customs/vies/viesdisc.do> to known the limitations of the EU validation service.
|
|
569 |
See L<http://ec.europa.eu/taxation_customs/vies/viesdisc.do> to known the limitations of the EU validation service.
|
478 | 570 |
|
479 | 571 |
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
480 | 572 |
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|