From 461c80a6b81500148152cf46ae30e43f4caff486 Mon Sep 17 00:00:00 2001 From: Martin Kamerbeek Date: Wed, 6 May 2009 13:33:16 +0000 Subject: [PATCH] Pluggable Tax: more POD, more tests, better code and less bugs --- docs/gotcha.txt | 4 + docs/upgrades/upgrade_7.7.5-7.7.6.pl | 11 + lib/WebGUI/Shop/Tax.pm | 13 +- lib/WebGUI/Shop/TaxDriver.pm | 5 + lib/WebGUI/Shop/TaxDriver/EU.pm | 270 ++++++++++++++++---- sbin/testEnvironment.pl | 1 + t/Shop/TaxDriver/EU.t | 355 +++++++++++++++++++++++++++ 7 files changed, 602 insertions(+), 57 deletions(-) create mode 100644 t/Shop/TaxDriver/EU.t diff --git a/docs/gotcha.txt b/docs/gotcha.txt index ece664d62..c5d1caf18 100644 --- a/docs/gotcha.txt +++ b/docs/gotcha.txt @@ -8,6 +8,10 @@ versions. Be sure to heed the warnings contained herein as they will save you many hours of grief. +7.7.6 +-------------------------------------------------------------------- + * WebGUI now requires Business::Tax::VAT::Validation. + 7.7.5 -------------------------------------------------------------------- * Due to a long standing bug in the Profile system, if the type of a diff --git a/docs/upgrades/upgrade_7.7.5-7.7.6.pl b/docs/upgrades/upgrade_7.7.5-7.7.6.pl index 9d49e3244..8be015eef 100644 --- a/docs/upgrades/upgrade_7.7.5-7.7.6.pl +++ b/docs/upgrades/upgrade_7.7.5-7.7.6.pl @@ -33,6 +33,7 @@ my $session = start(); # upgrade functions go here addTemplateAttachmentsTable($session); revertUsePacked( $session ); +addEuVatDbColumns( $session ); finish($session); @@ -77,6 +78,16 @@ sub revertUsePacked { print "DONE!\n" unless $quiet; } +#---------------------------------------------------------------------------- +sub addEuVatDbColumns { + my $session = shift; + print "\tAdding columns for improved VAT number checking..." unless $quiet; + + $session->db->write( 'alter table tax_eu_vatNumbers add column viesErrorCode int(3) default NULL' ); + + print "Done\n" unless $quiet; +} + # -------------- DO NOT EDIT BELOW THIS LINE -------------------------------- #---------------------------------------------------------------------------- diff --git a/lib/WebGUI/Shop/Tax.pm b/lib/WebGUI/Shop/Tax.pm index 6934423d7..f18189b13 100644 --- a/lib/WebGUI/Shop/Tax.pm +++ b/lib/WebGUI/Shop/Tax.pm @@ -128,10 +128,10 @@ A WebGUI::Session object. Required in class context, optional in instance contex sub getDriver { my $self = shift; my $session = shift || $self->session; - unless (defined $session && $session->isa("WebGUI::Session")) { - WebGUI::Error::InvalidObject->throw(expected=>"WebGUI::Session", got=>(ref $session), error=>"Need a session."); - } + WebGUI::Error::InvalidObject->throw( expected => "WebGUI::Session", got => (ref $session), error => "Need a session." ) + unless $session && $session->isa( 'WebGUI::Session' ); + my $className = $session->setting->get( 'activeTaxPlugin' ); my $driver = eval { WebGUI::Pluggable::instanciate( $className, 'new', [ $session ] ); @@ -155,9 +155,10 @@ Constructor for the WebGUI::Shop::Tax. Returns a WebGUI::Shop::Tax object. sub new { my $class = shift; my $session = shift; - unless (defined $session && $session->isa("WebGUI::Session")) { - WebGUI::Error::InvalidObject->throw(expected=>"WebGUI::Session", got=>(ref $session), error=>"Need a session."); - } + + WebGUI::Error::InvalidObject->throw( expected => "WebGUI::Session", got => (ref $session), error => "Need a session." ) + unless $session && $session->isa( 'WebGUI::Session' ); + my $self = {}; bless $self, $class; register $self; diff --git a/lib/WebGUI/Shop/TaxDriver.pm b/lib/WebGUI/Shop/TaxDriver.pm index a7113cbcb..cc9b3242b 100644 --- a/lib/WebGUI/Shop/TaxDriver.pm +++ b/lib/WebGUI/Shop/TaxDriver.pm @@ -105,6 +105,8 @@ sub className { my $self = shift; $self->session->log->fatal( "Tax plugin ($self) is required to overload the className method" ); + + return 'WebGUI::Shop:TaxDriver'; } #----------------------------------------------------------- @@ -214,6 +216,9 @@ sub new { my $class = shift; my $session = shift; + WebGUI::Error::InvalidObject->throw( expected => "WebGUI::Session", got => (ref $session), error => "Need a session." ) + unless $session && $session->isa( 'WebGUI::Session' ); + my $self = {}; bless $self, $class; register $self; diff --git a/lib/WebGUI/Shop/TaxDriver/EU.pm b/lib/WebGUI/Shop/TaxDriver/EU.pm index 04b67b63a..eae3d45fb 100644 --- a/lib/WebGUI/Shop/TaxDriver/EU.pm +++ b/lib/WebGUI/Shop/TaxDriver/EU.pm @@ -20,6 +20,8 @@ use SOAP::Lite; use WebGUI::Content::Account; use WebGUI::TabForm; use WebGUI::Utility qw{ isIn }; +use Business::Tax::VAT::Validation; +use Tie::IxHash; use base qw{ WebGUI::Shop::TaxDriver }; @@ -30,7 +32,8 @@ Package WebGUI::Shop::TaxDriver::EU =head1 DESCRIPTION This package manages tax information, and calculates taxes on a shopping cart specifically handling -European Union VAT taxes. +European Union VAT taxes. It allows you to define VAT groups (eg. in the Netherlands there are two VAT tariffs: +high (19%) and low (6%) ) that can be applied to SKU assets. =head1 SYNOPSIS @@ -44,7 +47,8 @@ These subroutines are available from this package: =cut -my $EU_COUNTRIES = { + +tie my %EU_COUNTRIES, 'Tie::IxHash', ( AT => 'Austria', BE => 'Belgium', BG => 'Bulgaria', @@ -72,7 +76,105 @@ my $EU_COUNTRIES = { SE => 'Sweden', SI => 'Slovenia', SK => 'Slovakia', -}; +); + +#------------------------------------------------------------------- + +=head2 addGroup ( name, rate ) + +Adds a tax group. Returns the group id. + +=head3 name + +The display name of the tax group. + +=head3 rate + +The tax rate for this group in percents. + +=cut + +sub addGroup { + my $self = shift; + my $name = shift; + my $rate = shift; + + WebGUI::Error::InvalidParam->throw( 'A group name is required' ) + unless $name; + WebGUI::Error::InvalidParam->throw( 'Group rate must be within 0 and 100' ) + unless defined $rate && $rate >= 0 && $rate <= 100; + + my $id = $self->session->id->generate; + my $groups = $self->get( 'taxGroups' ) || []; + + push @{ $groups }, { + name => $name, + rate => $rate, + id => $id, + }; + + $self->update( { taxGroups => $groups } ); + + return $id; +} + +#------------------------------------------------------------------- + +=head2 addVATNumber ( VATNumber, localCheckOnly ) + +Adds a VAT number to the database. Checks the number through the VIES database. Returns and error message if a +validation error occurred. If the number validates undef is returned. + +=head3 VATNumber + +The number that is to be added. + +=head3 user + +The user for which the number should be added. Defaults to the session user. + +=head3 localCheckOnly + +If set to a true value the the remote VAT number validation in the VIES database will not be preformed. The VAT +number will be checked against regexes, however. Mostly convenient for testing purposes. + +=cut + +sub addVATNumber { + my $self = shift; + my $number = shift; + my $user = shift || $self->session->user; + my $localCheckOnly = shift; + my $db = $self->session->db; + + WebGUI::Error::InvalidParam->throw( 'A VAT number is required' ) + unless $number; + WebGUI::Error::InvalidParam->throw( 'The second argument must be an instanciated WebGUI::User object' ) + unless ref $user eq 'WebGUI::User'; + WebGUI::Error::InvalidParam->throw( 'Visitor cannot add VAT numbers' ) + if $user->isVisitor; + + # Check number + my $validator = Business::Tax::VAT::Validation->new; + my $numberIsValid = $localCheckOnly ? $validator->local_check( $number ) : $validator->check( $number ); + + # Number contains syntax error does not exist. Do not write the code to the db. + if ( !$numberIsValid && $validator->get_last_error_code <= 16 ) { + return 'The entered VAT number is invalid.'; + } + + + # Write the code to the db. + $db->write( 'replace into tax_eu_vatNumbers (userId,countryCode,vatNumber,approved,viesErrorCode) values (?,?,?,?,?)', [ + $user->userId, + substr( $number, 0 , 2 ), + $number, + $numberIsValid ? 1 : 0, + $numberIsValid ? undef : $validator->get_last_error_code, + ] ); + + return $numberIsValid ? undef : 'Number validation currently not available. Check later.'; +} #------------------------------------------------------------------- @@ -86,6 +188,59 @@ sub className { return 'WebGUI::Shop::TaxDriver::EU'; } +#------------------------------------------------------------------- + +=head2 deleteGroup ( groupId ) + +Deletes a tax group. + +=head3 groupId + +The id of the tax group that is to be deleted. + +=cut + +sub deleteGroup { + my $self = shift; + my $removeGroupId = shift; + + WebGUI::Error::InvalidParam->throw( 'A group id is required' ) + unless $removeGroupId; + + my $taxGroups = $self->get( 'taxGroups' ); + my @newGroups = grep { $_->{ id } ne $removeGroupId } @{ $taxGroups }; + + $self->update( { taxGroups => \@newGroups } ); +} + +#----------------------------------------------------------- + +=head2 deleteVATNumber ( VATNumber, [ user ] ) + +Deletes a VAT number. + +=head3 VATNumber + +The VATNumber to delete. + +=head3 user + +The user whose VATNumber must be deleted, in the form of a WebGUI::User object. + +=cut + +sub deleteVATNumber { + my $self = shift; + my $number = shift; + my $user = shift || $self->session->user; + my $session = $self->session; + + $session->db->write( 'delete from tax_eu_vatNumbers where userId=? and vatNumber=?', [ + $user->userId, + $number, + ] ); +} + #----------------------------------------------------------- =head2 getConfigurationScreen ( ) @@ -99,7 +254,12 @@ sub getConfigurationScreen { my $session = $self->session; my $taxGroups = $self->get( 'taxGroups' ) || []; - + + tie my %countryOptions, 'Tie::IxHash', ( + '' => ' - select a country - ', + %EU_COUNTRIES, + ); + # General setting form my $f = WebGUI::HTMLForm->new( $session ); $f->hidden( @@ -119,7 +279,7 @@ sub getConfigurationScreen { value => $self->get( 'shopCountry' ), label => 'Residential country', hoverHelp => 'The country where your shop resides.', - options => $EU_COUNTRIES, + options => \%countryOptions, ); $f->submit; my $general = $f->print; @@ -203,12 +363,12 @@ sub getCountryCode { my $countryName = shift; # Do reverse lookup on eu countries hash - return { reverse %{ $EU_COUNTRIES } }->{ $countryName }; + return { reverse %EU_COUNTRIES }->{ $countryName }; } #------------------------------------------------------------------- -=head2 getCountryName ($countryCode) +=head2 getCountryName ( $countryCode ) Given a 2 character country code, return the name of the country. @@ -222,15 +382,19 @@ sub getCountryName { my $self = shift; my $countryCode = shift; - return $EU_COUNTRIES->{ $countryCode }; + return $EU_COUNTRIES{ $countryCode }; } #------------------------------------------------------------------- -=head2 getGroupRate ($taxGroupId) +=head2 getGroupRate ( $taxGroupId ) + +Returns the tax rate for a given tax group. =head3 $taxGroupId +The id of the tax group whose rate should be returned. + =cut sub getGroupRate { @@ -303,7 +467,7 @@ sub getUserScreen { #------------------------------------------------------------------- -=head2 getTaxRate ( sku, [ address ] ) +=head2 getTaxRate ( sku, [ address, user ] ) Returns the tax rate in percents (eg. 19 for a rate of 19%) for the given sku and shipping address. Implements EU VAT taxes and group rates. @@ -315,6 +479,11 @@ sub getTaxRate { my $sku = shift; my $address = shift; + WebGUI::Error::InvalidParam->throw(error => 'Must pass in a WebGUI::Asset::Sku object') + unless $sku && $sku->isa( 'WebGUI::Asset::Sku' ); + WebGUI::Error::InvalidParam->throw(error => 'Must pass in a WebGUI::Shop::Address object') + if $address && !$address->isa( 'WebGUI::Shop::Address' ); + my $config = $sku->getTaxConfiguration( $self->className ); # Fetch the tax group from the sku. If the sku has none, use the default tax group. @@ -340,26 +509,34 @@ sub getTaxRate { #------------------------------------------------------------------- -=head2 getVATNumbers ($countryCode) +=head2 getVATNumbers ( $countryCode ) + +Returns an array ref of hash refs containing the properties of the VAT numbers a user s registered for a given +country. Returns an empty array ref if the user has no VAT numbers in the requested country. + +The hash keys of interest for most people are vatNumber, which contains the actual number, and approved, which +indicates whether or not the number has been approved for use yet. =head3 $countryCode +The two letter country code of the country the VAT numbers are requested for. + =cut sub getVATNumbers { my $self = shift; my $countryCode = shift; - my $session = $self->session; + my $user = shift || $self->session->user; my $sql = 'select * from tax_eu_vatNumbers where userId=?'; - my $placeHolders = [ $session->user->userId ]; + my $placeHolders = [ $user->userId ]; if ( $countryCode ) { $sql .= ' and countryCode=?'; push @{ $placeHolders }, $countryCode; } - my $numbers = $session->db->buildArrayRefOfHashRefs( $sql, $placeHolders ); + my $numbers = $self->session->db->buildArrayRefOfHashRefs( $sql, $placeHolders ); return $numbers; } @@ -368,8 +545,12 @@ sub getVATNumbers { =head2 hasVATNumber ($countrycode) +Returns a boolean indicating whether or not the user has VAT numbers registered for the given country. + =head3 $countryCode +The two letter country code of contry for which the existance of VAT numbers is requested. + =cut sub hasVATNumber { @@ -417,6 +598,8 @@ sub skuFormDefinition { =head2 www_addGroup +Adds a VAT group. + =cut sub www_addGroup { @@ -425,18 +608,7 @@ sub www_addGroup { return $self->session->privilege->insufficient unless $self->canManage; - my $groups = $self->get( 'taxGroups' ) || []; - my $name = $form->process( 'name' ); - my $rate = $form->process( 'rate' ); - my $id = $self->session->id->generate; - - push @{ $groups }, { - name => $name, - rate => $rate, - id => $id, - }; - - $self->update( { taxGroups => $groups } ); + $self->addGroup( $form->process( 'name' ), $form->process( 'rate' ) ); return ''; } @@ -445,6 +617,11 @@ sub www_addGroup { =head2 www_addVATNumber +Allows a user to add a VAT number. The validity of VAT numbers will be automatically checked using the VIES service +provided by the European Union. See http://ec.europa.eu/taxation_customs/vies/vieshome.do for more information +conerning the service. Please also read the disclamer information located at +http://ec.europa.eu/taxation_customs/vies/viesdisc.do. + =cut sub www_addVATNumber { @@ -457,22 +634,12 @@ sub www_addVATNumber { my $vatNumber = uc $form->process( 'vatNumber' ); my ($countryCode, $number) = $vatNumber =~ m/^([A-Z]{2})([A-Z0-9]+)$/; - return 'Illegal country code' unless isIn( $countryCode, keys %{ $EU_COUNTRIES } ); - + return 'Illegal country code' unless isIn( $countryCode, keys %EU_COUNTRIES ); + return 'You already have a VAT number for this country.' if @{ $self->getVATNumbers( $countryCode ) }; - # Check VAT number via SOAP interface. - # TODO: Handle timeouts. - my $soap = SOAP::Lite->service('http://ec.europa.eu/taxation_customs/vies/api/checkVatPort?wsdl'); - my $isValid = ( $soap->checkVat( $countryCode, $number ) )[ 3 ] || 0; - - # Write the code to the db. - $db->write( 'replace into tax_eu_vatNumbers (userId,countryCode,vatNumber,approved) values (?,?,?,?)', [ - $self->session->user->userId, - $countryCode, - $vatNumber, - $isValid, - ] ); + #### TODO: Handle errorMessage. + my $errorMessage = $self->addVATNumber( $vatNumber ); my $instance = WebGUI::Content::Account->createInstance($session,"shop"); return $instance->displayContent( $instance->callMethod("manageTaxData", [], $session->user->userId) ); @@ -482,6 +649,8 @@ sub www_addVATNumber { =head2 www_deleteGroup +Deletes a VAT group. + =cut sub www_deleteGroup { @@ -490,11 +659,7 @@ sub www_deleteGroup { return $self->session->privilege->insufficient unless $self->canManage; - my $taxGroups = $self->get( 'taxGroups' ); - my $removeGroupId = $form->process( 'groupId' ); - my @newGroups = grep { $_->{ id } ne $removeGroupId } @{ $taxGroups }; - - $self->update( { taxGroups => \@newGroups } ); + $self->deleteGroup( $form->process( 'groupId' ) ); return ''; } @@ -503,19 +668,18 @@ sub www_deleteGroup { =head2 www_deleteVATNumber +Deletes a VAT number. + =cut sub www_deleteVATNumber { my $self = shift; my $session = $self->session; - return $session->privilege->insufficient unless $session->user->isVisitor; - - $session->db->write( 'delete from tax_eu_vatNumbers where userId=? and vatNumber=?', [ - $session->user->userId, - $session->form->process( 'vatNumber' ), - ] ); + return $session->privilege->insufficient if $session->user->isVisitor; + $self->deleteVATNumber( $session->form->process( 'vatNumber' ) ); + my $instance = WebGUI::Content::Account->createInstance($session,"shop"); return $instance->displayContent( $instance->callMethod("manageTaxData", [], $session->user->userId) ); } @@ -524,6 +688,8 @@ sub www_deleteVATNumber { =head2 www_saveConfiguration +Updates the configuration properties for this plugin, as passed by the form on the configuration screen. + =cut sub www_saveConfiguration { @@ -543,6 +709,8 @@ sub www_saveConfiguration { =head2 www_setDefaultGroup +Sets a VAT group to be used as default for SKU's that do not have a VAT group defined yet. + =cut sub www_setDefaultGroup { diff --git a/sbin/testEnvironment.pl b/sbin/testEnvironment.pl index 0a130bbc6..4803a4cba 100755 --- a/sbin/testEnvironment.pl +++ b/sbin/testEnvironment.pl @@ -126,6 +126,7 @@ checkModule("Clone", "0.31" ); checkModule('HTML::Packer', "0.4" ); checkModule('JavaScript::Packer', '0.02' ); checkModule('CSS::Packer', '0.2' ); +checkModule('Business::Tax::VAT::Validation', '0.20' ); failAndExit("Required modules are missing, running no more checks.") if $missingModule; diff --git a/t/Shop/TaxDriver/EU.t b/t/Shop/TaxDriver/EU.t new file mode 100644 index 000000000..0d4b2085f --- /dev/null +++ b/t/Shop/TaxDriver/EU.t @@ -0,0 +1,355 @@ +# vim:syntax=perl +#------------------------------------------------------------------- +# WebGUI is Copyright 2001-2009 Plain Black Corporation. +#------------------------------------------------------------------- +# Please read the legal notices (docs/legal.txt) and the license +# (docs/license.txt) that came with this distribution before using +# this software. +#------------------------------------------------------------------ +# http://www.plainblack.com info@plainblack.com +#------------------------------------------------------------------ + +# Write a little about what this script tests. +# +# + +use FindBin; +use strict; +use lib "$FindBin::Bin/../../lib"; +use Test::More; +use Test::Deep; +use Exception::Class; +use Data::Dumper; + +use WebGUI::Test; # Must use this before any other WebGUI modules +use WebGUI::Session; +use WebGUI::Text; +use WebGUI::Shop::Cart; +use WebGUI::Shop::AddressBook; + +#---------------------------------------------------------------------------- +# Init +my $session = WebGUI::Test->session; + +my $taxUser = WebGUI::User->new( $session, 'new' ); +$taxUser->username( 'MrEvasion' ); + + +#---------------------------------------------------------------------------- +# Tests + +my $tests = 44; +plan tests => 1 + $tests; + +#---------------------------------------------------------------------------- +# put your tests here + +my $loaded = use_ok('WebGUI::Shop::TaxDriver::EU'); + +SKIP: { + + skip 'Unable to load module WebGUI::Shop::TaxDriver::EU', $tests unless $loaded; + + ####################################################################### + # + # new + # + ####################################################################### + + my $taxer = WebGUI::Shop::TaxDriver::EU->new($session); + + isa_ok($taxer, 'WebGUI::Shop::TaxDriver::EU'); + + isa_ok($taxer->session, 'WebGUI::Session', 'session method returns a session object'); + + is($session->getId, $taxer->session->getId, 'session method returns OUR session object'); + + ####################################################################### + # + # className + # + ####################################################################### + + is( $taxer->className, 'WebGUI::Shop::TaxDriver::EU', 'className returns correct class name' ); + + ####################################################################### + # + # getConfigurationScreen + # + ####################################################################### + + #### TODO: Figure out how to test this. + + ####################################################################### + # + # getCountryCode + # + ####################################################################### + + is( $taxer->getCountryCode( 'Netherlands' ), 'NL', 'getCountryCode returns correct code for country inside EU.' ); + is( $taxer->getCountryCode( 'United States' ), undef, 'getCountryCode returns undef for countries outside EU.' ); + + ####################################################################### + # + # getCountryName + # + ####################################################################### + + is( $taxer->getCountryName( 'NL' ), 'Netherlands', 'getCountryName returns correct name for country code within EU.' ); + is( $taxer->getCountryName( 'US' ), undef, 'getCountryName returns undef for county codes outside EU.' ); + + ####################################################################### + # + # addVATNumber + # + ####################################################################### + + $session->user( {userId=>$taxUser->userId} ); + + my $testVAT_NL = 'NL123456789B12'; + my $testVAT_BE = 'BE0123456789'; + my $invalidVAT = 'ByNoMeansAllowed'; + my $visitorUser = WebGUI::User->new( $session, 1 ); + + eval { $taxer->addVATNumber }; + my $e = Exception::Class->caught(); + isa_ok( $e, 'WebGUI::Error::InvalidParam', 'A VAT number is required' ); + is( $e, 'A VAT number is required', 'addVATNumber returns correct message for missing VAT number' ); + + eval { $taxer->addVATNumber( $testVAT_NL, 'NotAUserObject' ) }; + $e = Exception::Class->caught(); + isa_ok( $e, 'WebGUI::Error::InvalidParam', 'Second argument must be a user object' ); + is( $e, 'The second argument must be an instanciated WebGUI::User object', 'addVATNumber returns correct message when user object is of wrong type' ); + + eval { $taxer->addVATNumber( $testVAT_NL, $visitorUser ) }; + $e = Exception::Class->caught(); + isa_ok( $e, 'WebGUI::Error::InvalidParam', 'User may not be visitor' ); + is( $e, 'Visitor cannot add VAT numbers', 'addVATNumber returns correct message when user is visitor' ); + + my $response = $taxer->addVATNumber( $invalidVAT, $taxUser, 1 ); + is( $response, 'The entered VAT number is invalid.', 'Invalid VAT numbers return an error message' ); + + my $responseNL = $taxer->addVATNumber( $testVAT_NL, $taxUser, 1 ); + my $responseBE = $taxer->addVATNumber( $testVAT_BE, $taxUser, 1 ); + + ok( !defined $responseNL && !defined $responseBE, 'Valid VAT numbers return undef.' ); + + ####################################################################### + # + # getVATNumbers + # + ####################################################################### + + my $expectNL = { + userId => $taxUser->userId, + countryCode => 'NL', + vatNumber => $testVAT_NL, + approved => 1, + viesErrorCode => undef, + }; + my $expectBE = { + userId => $taxUser->userId, + countryCode => 'BE', + vatNumber => $testVAT_BE, + approved => 1, + viesErrorCode => undef, + }; + + my $vatNumbers = $taxer->getVATNumbers( undef, $taxUser ); + cmp_bag( $vatNumbers, [ $expectNL, $expectBE ], 'VAT Numbers are correctly returned by getVATNumbers' ); + + $vatNumbers = $taxer->getVATNumbers( 'BE', $taxUser ); + cmp_bag( $vatNumbers, [ $expectBE ], 'getVATNumbers filters on country code when one is passed' ); + + ####################################################################### + # + # deleteVATNumber + # + ####################################################################### + + $taxer->deleteVATNumber( $testVAT_BE, $taxUser ); + $vatNumbers = $taxer->getVATNumbers( undef, $taxUser ); + cmp_bag( $vatNumbers, [ $expectNL ], 'deleteVATNumber deletes number' ); + + $taxer->deleteVATNumber( $testVAT_NL, $taxUser ); + ####################################################################### + # + # addGroupRate + # + ####################################################################### + + eval { $taxer->addGroup }; + $e = Exception::Class->caught(); + isa_ok( $e, 'WebGUI::Error::InvalidParam', 'addGroup requires a group name' ); + is( $e, 'A group name is required', 'addGroup returns correct message for omitted group name' ); + + eval { $taxer->addGroup( 'Dummy' ) }; + $e = Exception::Class->caught(); + isa_ok( $e, 'WebGUI::Error::InvalidParam', 'addGroup requires a tax rate' ); + is( $e, 'Group rate must be within 0 and 100', 'addGroup returns correct message on omitted tax rate' ); + + eval { $taxer->addGroup( 'Dummy', -1 ) }; + $e = Exception::Class->caught(); + isa_ok( $e, 'WebGUI::Error::InvalidParam', 'addGroup: tax rate cannot be < 0' ); + is( $e, 'Group rate must be within 0 and 100', 'addGroup returns correct message on tax rate < 0' ); + + eval { $taxer->addGroup( 'Dummy', 101 ) }; + $e = Exception::Class->caught(); + isa_ok( $e, 'WebGUI::Error::InvalidParam', 'addGroup: tax rate cannot be > 100' ); + is( $e, 'Group rate must be within 0 and 100', 'addGroup returns correct message on tax rate > 100' ); + + my $id0 = eval { $taxer->addGroup( 'Group0', 0 ) }; + $e = Exception::Class->caught(); + ok( !$e, 'addGroup: 0% is a valid group rate' ); + + my $id100 = eval { $taxer->addGroup( 'Group100', 100 ) }; + $e = Exception::Class->caught(); + ok( !$e, 'addGroup: 100% is a valid group rate' ); + + my $id50_5 = eval { $taxer->addGroup( 'Group50.5', 50.5 ) }; + $e = Exception::Class->caught(); + ok( !$e, 'addGroup: floats are a valid group rate' ); + + my $taxGroups = $taxer->get( 'taxGroups' ); + my $expectGroups = [ + { + name => 'Group0', + rate => 0, + id => $id0, + }, + { + name => 'Group100', + rate => 100, + id => $id100, + }, + { + name => 'Group50.5', + rate => 50.5, + id => $id50_5, + }, + ]; + cmp_bag( $taxGroups, $expectGroups, 'addGroup saves correctly' ); + + + ####################################################################### + # + # getGroupRate + # + ####################################################################### + + ok( $taxer->getGroupRate( $id0 ) == 0 + && $taxer->getGroupRate( $id100 ) == 100 + && $taxer->getGroupRate( $id50_5 ) == 50.5, + 'getGroup rate gets correct rates' + ); + + ####################################################################### + # + # getTaxRate + # + ####################################################################### + + my $book = WebGUI::Shop::AddressBook->create($session); + + # setup address in EU but not in residential country of merchant + my $beAddress = $book->addAddress({ + label => 'BE', + city => 'Antwerpen', + country => 'Belgium', + }); + + # setup address in residential country of merchant + my $nlAddress = $book->addAddress({ + label => 'NL', + city => 'Delft', + country => 'Netherlands', + }); + + # setup address outside EU + my $usAddress = $book->addAddress({ + label => 'outside eu', + city => 'New Amsterdam', + country => 'US', + }); + + eval { $taxer->getTaxRate(); }; + $e = Exception::Class->caught(); + isa_ok($e, 'WebGUI::Error::InvalidParam', 'getTaxRate: error handling for not sending a sku'); + is($e->error, 'Must pass in a WebGUI::Asset::Sku object', 'getTaxRate: error handling for not sending a sku'); + + # Build a cart, add some Donation SKUs to it. Set one to be taxable. + my $cart = WebGUI::Shop::Cart->newBySession( $session ); + + my $sku = WebGUI::Asset->getRoot($session)->addChild( { + className => 'WebGUI::Asset::Sku::Donation', + title => 'Taxable donation', + defaultPrice => 100.00, + } ); + + # Set defaultTaxGroup and residential country + $taxer->update( { defaultGroup => $id50_5, shopCountry => 'NL' } ); + + # Check default tax group + is( $taxer->getTaxRate( $sku ), 50.5, 'getTaxRate returns default tax group when no address is given and sku has no tax group set'); + + # Check case when no address is given + $sku->setTaxConfiguration( 'WebGUI::Shop::TaxDriver::EU', { taxGroup => $id100 } ); + is( $taxer->getTaxRate( $sku ), 100, 'getTaxRate returns tax group set by sku when no address is given'); + + # Address outside EU + is( $taxer->getTaxRate( $sku, $usAddress ), 0, 'getTaxRate: shipping addresses outside EU are tax exempt' ); + + # Addresses inside EU + is( $taxer->getTaxRate( $sku, $beAddress ), 100, 'getTaxRate: shipping addresses inside EU w/o VAT number pay tax' ); + is( $taxer->getTaxRate( $sku, $nlAddress ), 100, 'getTaxRate: shipping addresses in country of merchant w/o VAT number pay tax' ); + + # Add VAT numbers + $taxer->addVATNumber( $testVAT_NL, $taxUser, 1); + $taxer->addVATNumber( $testVAT_BE, $taxUser, 1); + + is( $taxer->getTaxRate( $sku, $beAddress ), 0, + 'getTaxRate: shipping addresses inside EU but other country than merchant w/ VAT number are tax exempt.' + ); + is( $taxer->getTaxRate( $sku, $nlAddress ), 100, 'getTaxRate: shipping addresses in country of merchant w/ VAT number pay tax' ); + + ####################################################################### + # + # deleteGroup + # + ####################################################################### + + eval { $taxer->deleteGroup }; + $e = Exception::Class->caught(); + isa_ok( $e, 'WebGUI::Error::InvalidParam', 'addGroup requires a group id' ); + is( $e, 'A group id is required', 'addGroup returns correct message for missing group id' ); + + $taxer->deleteGroup( $id50_5 ); + + $taxGroups = $taxer->get( 'taxGroups' ); + cmp_bag( $taxGroups, [ + { + name => 'Group0', + rate => 0, + id => $id0, + }, + { + name => 'Group100', + rate => 100, + id => $id100, + }, + ], 'deleteGroup deletes correctly' ); + + + +} + +#---------------------------------------------------------------------------- +# Cleanup +END { + $session->db->write('delete from tax_eu_vatNumbers'); + $session->db->write('delete from cart'); + $session->db->write('delete from addressBook'); + $session->db->write('delete from address'); + + $taxUser->delete; +}