From b21c5f438942c72f7df07763cca0fd69a51fdb73 Mon Sep 17 00:00:00 2001 From: Martin Kamerbeek Date: Fri, 17 Apr 2009 13:35:20 +0000 Subject: [PATCH] Forgot to 'svn add' a bunch of files. --- ...ort_account_shop_shop-account-layout.wgpkg | Bin 0 -> 1278 bytes lib/WebGUI/Shop/TaxDriver.pm | 220 ++++++ lib/WebGUI/Shop/TaxDriver/EU.pm | 419 +++++++++++ lib/WebGUI/Shop/TaxDriver/Generic.pm | 688 ++++++++++++++++++ 4 files changed, 1327 insertions(+) create mode 100644 docs/upgrades/packages-7.7.4/root_import_account_shop_shop-account-layout.wgpkg create mode 100644 lib/WebGUI/Shop/TaxDriver.pm create mode 100644 lib/WebGUI/Shop/TaxDriver/EU.pm create mode 100644 lib/WebGUI/Shop/TaxDriver/Generic.pm diff --git a/docs/upgrades/packages-7.7.4/root_import_account_shop_shop-account-layout.wgpkg b/docs/upgrades/packages-7.7.4/root_import_account_shop_shop-account-layout.wgpkg new file mode 100644 index 0000000000000000000000000000000000000000..e33db53128b9aaba60244482ee868a1460f30c26 GIT binary patch literal 1278 zcmVc7UfZio zV!&G`&e;x>JMDkJu@jPz(so;Ir8}vSN?<%Qe&(4Od%9V9`Ny{HR-;j=*mk|uu=6*{ z9=lSn*IQ2AcD9^W1;LL-vx30n<)2t8EC5=7g`(m8`#rV}LBgYhOWyZ;`cJJGwDrQL zW4<>M$>ul7AK>H%BLQE{?d?48%Q{HY-)ObU`ES^*tqKA*U;cTi{BOQI^q;ZAKW`ul zRjIhZ6H-MTbaD08h$6uw0xdzLmKrs!=?Q`r@jooIw7OL_%6fresdjH8E?~81Y{Hna zY#F*t8UNKN9x*DX#LsO*GC!xG&*#ReoiE5m$RkPby5sYZh%-q<-!B6pw@Tmxg8d<$ z7{jW|Xn2DJVZU8Mqzgu1ccLz2G8xo*UuT1p#k;-Fxo#|&!Mtu)IpvY1KWZtC8p8`7 ztK6E(1M2%EG%o5z`9N^RHPAAP6T#!C@9+9lC00&urbrO6WolfX&X5P0ln^U?_ZY$A zvT9=0&FaQ1TRz`cJR0H=u#>GD9R&`r8Rrp7Loh#9br|iscuBu&EM{0BV5Y0driy8{ z^93W(ONoe%5tThp(HZG3;Yz}Y2ek)gmT^eO-L5<8mYuj3$Se$h0755GgVs-KrjciI z*NOoVD7hBUk$l^z)h+4bVa?PPU^bL>(1@$)do(3J#dtGK<>r` z34J=gs?}E9ZS;4qx*&UsGZ&!T@mEaY`M!)tA9<)q2<4vaS6pAjv?gz8Sd%y?q|soY zK14I0^pbW+5}hSq8-{oWHD53@Bo^JD?;AEiRMIkV+_3A)cQ zbuBPlTbP;y{lGDn^Dv`iev08Jdp-f$#kJHv9t(YejM`H|e#qpBp(e}^@$K_NO>?dA z?PH;u@UPH{N7Z)`c@a3E6{Nc2ckcedK?*0$3q%r@rw zF{`@GQQGt9uyi%2_w4V`S4O#&W2REyzIenE(1m*&&fn;mD*)h`qo&MIK*D&aFT}_9 z*E3d}j667k`E6F5vCM>WO8-s)wCe?Q5f(5?nGNotv(q^k9v$8#m&!x#-7JY+nfR#O zA!8h~J2^L_V44uKC-B{FH(E|%@DpT4B^;|-ifnGj)Kb5 zg@pO4oKX|olCt6m4Mk!G=yhdc=I&KOoMy9WZ*R5p9U+g my %session; +readonly messages => my %messages; +private options => my %options; + +#----------------------------------------------------------- +sub appendTaxDetailVars { + my $self = shift; + my $var = shift; + + return $var; +} + +#----------------------------------------------------------- +sub canManage { + my $self = shift; + my $admin = WebGUI::Shop::Admin->new( $self->session ); + + return $admin->canManage; +} + +#----------------------------------------------------------- + +=head2 className { + +Returns the class name of your plugin. You must overload this method in you own plugin. + +=cut + +sub className { + my $self = shift; + + $self->session->log->fatal( "Tax plugin ($self) is required to overload the className method" ); +} + +#----------------------------------------------------------- + +=head2 get ( [ property ] ) + +Returns the value of the requested configuration property. Returns a hash ref of all property/value pairs when no +specific property is passed. + +=head3 property + +The property whose value should be returned. + +=cut + +sub get { + my $self = shift; + my $key = shift; + + my $options = $options{ id $self }; + + # Return safe copy of options hash if no key is passed. + return { %{ $options } } unless $key; + + # Return option if key is passed. + return $options->{ $key } if exists $options->{ $key }; + + # Key does not exist. + $self->session->log->warn( "Non-existant option [$key] was queried by tax plugin $self" ); + return undef; +} + +#----------------------------------------------------------- + +=head2 getConfigurationScreen ( ) + +Returns the configuration screen that contains the configuration options for this plugin in the admin console. + +=cut + +sub getConfigurationScreen { + return 'This plugin has no configuration options'; +} + +#----------------------------------------------------------- + +=head2 getTaxRate ( sku, [ address ] ) + +Returns the tax rate in percents (eg. 19 for a rate of 19%) for the given sku and shipping address. Your tax driver +must overload this method. + +Note that address is optional and that it's up to your plugin to handle that case. + +=head3 sku + +The sku for which the tax rate must be determined. Should be a WebGUI::Asset::Sku::* instance. + +=head3 address + +Optional, the shipping address for which to calculate the tax. Must be an instance of WebGUI::Shop::Address. + +=head + +=cut + +sub getTaxRate { + my $self = shift; + + $self->session->log->fatal("Tax plugin ". $self->className ." is required to overload getTaxRate"); +} + +#----------------------------------------------------------- + +=head2 getUserScreen ( ) + +Returns the screen for entering per user configuration for this tax driver. + +=cut + +sub getUserScreen { + return 'There are no tax options to configure.'; +} + +#----------------------------------------------------------- + +=head2 skuFormDefinition ( ) + +Returns a hash ref containing the form defintion for the per sku options for this tax driver. + +=cut + +sub skuFormDefinition { + return {}; +} + +#------------------------------------------------------------------- + +=head2 new ( $session ) + +Constructor + +=head3 session + +Instanciated WebGUI::Session object. + +=cut + +sub new { + my $class = shift; + my $session = shift; + + my $self = {}; + bless $self, $class; + register $self; + + my $id = id $self; + $session{ $id } = $session; + $messages{ $id } = []; + + # Load plugin configuration + my $optionsJSON = $session->db->quickScalar( 'select options from taxDriver where className=?', [ + $self->className, + ] ); + $options{ $id } = $optionsJSON ? from_json( $optionsJSON ) : {}; + + return $self; +} + +#------------------------------------------------------------------- + +=head2 processSkuFormPost ( ) + +Processes the form parameters defined in the skuFormDefinition method and returns a hash ref containing the result. + +=cut + +sub processSkuFormPost { + my $self = shift; + my $form = $self->session->form; + my $configuration = {}; + + my $definition = $self->skuFormDefinition; + + foreach my $fieldName ( keys %{ $definition } ) { + my ($fieldType, $defaultValue) = @{ $definition->{ $fieldName } }{ qw{ fieldType defaultValue } }; + + $configuration->{ $fieldName } = $form->process( $fieldName, $fieldType, $defaultValue ); + } + + return $configuration; +} + +#----------------------------------------------------------- + +=head2 update ( properties ) + +Updates the properties of the tax driver according to those passed. + +=head3 properties + +Hash ref containing the properties to set. + +=cut + +sub update { + my $self = shift; + my $update = shift; + my $db = $self->session->db; + + # update local options hash + $options{ id $self } = { %{ $options{ id $self } }, %{ $update } }; + + # Persist to db + $db->write( 'replace into taxDriver (className, options) values (?,?)', [ + $self->className, + to_json( $options{ id $self } ), + ] ); +} + +1; + diff --git a/lib/WebGUI/Shop/TaxDriver/EU.pm b/lib/WebGUI/Shop/TaxDriver/EU.pm new file mode 100644 index 000000000..ab9e10874 --- /dev/null +++ b/lib/WebGUI/Shop/TaxDriver/EU.pm @@ -0,0 +1,419 @@ +package WebGUI::Shop::TaxDriver::EU; + +use strict; + +use SOAP::Lite; +use WebGUI::Content::Account; +use WebGUI::TabForm; +use WebGUI::Utility qw{ isIn }; + +use base qw{ WebGUI::Shop::TaxDriver }; + +my $EU_COUNTRIES = { + AT => 'Austria', + BE => 'Belgium', + BG => 'Bulgaria', + CY => 'Cyprus', + CZ => 'Czech Republic', + DE => 'Germany', + DK => 'Denmark', + EE => 'Estonia', + EL => 'Greece', + ES => 'Spain', + FI => 'Finland', + FR => 'France ', + GB => 'United Kingdom', + HU => 'Hungary', + IE => 'Ireland', + IT => 'Italy', + LT => 'Lithuania', + LU => 'Luxembourg', + LV => 'Latvia', + MT => 'Malta', + NL => 'Netherlands', + PL => 'Poland', + PT => 'Portugal', + RO => 'Romania', + SE => 'Sweden', + SI => 'Slovenia', + SK => 'Slovakia', +}; + +#------------------------------------------------------------------- +sub className { + return 'WebGUI::Shop::TaxDriver::EU'; +} + +#------------------------------------------------------------------- +sub getConfigurationScreen { + my $self = shift; + my $session = $self->session; + + my $taxGroups = $self->get( 'taxGroups' ) || []; + + # General setting form + my $f = WebGUI::HTMLForm->new( $session ); + $f->hidden( + name => 'shop', + value => 'tax', + ); + $f->hidden( + name => 'method', + value => 'do', + ); + $f->hidden( + name => 'do', + value => 'saveConfiguration', + ); + $f->selectBox( + name => 'shopCountry', + value => $self->get( 'shopCountry' ), + label => 'Residential country', + hoverHelp => 'The country where your shop resides.', + options => $EU_COUNTRIES, + ); + $f->submit; + my $general = $f->print; + + # VAT groups manager + my $vatGroups = 'VAT groups
'; + $vatGroups .= q{}; + foreach my $group ( @{ $taxGroups} ) { + my $deleteUrl = $session->url->page('shop=tax;method=do;do=deleteGroup;groupId=' . $group->{ id }); + my $makeDefaultUrl = $session->url->page('shop=tax;method=do;do=setDefaultGroup;groupId=' . $group->{ id }); + + $vatGroups .= + q{}; + } + $vatGroups .= q{
Group nameRate
} + . join( '', + $group->{ name } . ( $group->{ id } eq $self->get( 'defaultGroup' ) ? '(default)' : '' ), + $group->{ rate }, + qq{delete}, + qq{Set as default group}, + ) + . q{
}; + $vatGroups .= + WebGUI::Form::formHeader( $session ) + . WebGUI::Form::hidden( $session, { name => 'shop', value => 'tax' } ) + . WebGUI::Form::hidden( $session, { name => 'method', value => 'do' } ) + . WebGUI::Form::hidden( $session, { name => 'do', value => 'addGroup' } ) + . 'Name ' + . WebGUI::Form::text( $session, { name => 'name' } ) + . ' Rate ' + . WebGUI::Form::float( $session, { name => 'rate' } ) + . '%' + . WebGUI::Form::submit( $session, { value => 'Add' } ) + . WebGUI::Form::formFooter( $session ); + + # Wrap output in a YUI Tab widget. + my ($style, $url) = $session->quick( qw{ style url } ); + $style->setLink($self->{_css},{rel=>"stylesheet", rel=>"stylesheet",type=>"text/css"}); + $style->setLink($url->extras('/yui/build/fonts/fonts-min.css'),{type=>"text/css", rel=>"stylesheet"}); + $style->setLink($url->extras('/yui/build/tabview/assets/skins/sam/tabview.css'),{type=>"text/css", rel=>"stylesheet"}); + $style->setLink($url->extras('/yui/build/container/assets/container.css'),{ type=>'text/css', rel=>"stylesheet" }); + $style->setLink($url->extras('/hoverhelp.css'),{ type=>'text/css', rel=>"stylesheet" }); + $style->setScript($url->extras('/yui/build/utilities/utilities.js'),{ type=>'text/javascript' }); + $style->setScript($url->extras('/yui/build/container/container-min.js'),{ type=>'text/javascript' }); + $style->setScript($url->extras('/yui/build/tabview/tabview-min.js'),{ type=>'text/javascript' }); + $style->setScript($url->extras('/hoverhelp.js'),{ type=>'text/javascript' }); + + my $output = < +
+ +
+
$general
+
$vatGroups
+
+
+ + +EOHTML + + return $output; +} + +#------------------------------------------------------------------- +sub getCountryCode { + my $self = shift; + my $countryName = shift; + + # Do reverse lookup on eu countries hash + return { reverse %{ $EU_COUNTRIES } }->{ $countryName }; +} + +#------------------------------------------------------------------- +sub getCountryName { + my $self = shift; + my $countryCode = shift; + + return $EU_COUNTRIES->{ $countryCode }; +} + +#------------------------------------------------------------------- +sub getGroupRate { + my $self = shift; + my $taxGroupId = shift; + + my $taxGroups = $self->get( 'taxGroups' ); + my ($group) = grep { $_->{ id } eq $taxGroupId } @{ $taxGroups }; + + return $group->{ rate }; +} + +#------------------------------------------------------------------- +sub getUserScreen { + my $self = shift; + my $url = $self->session->url; + + + my $output = 'VAT Numbers
' + . ''; + + foreach my $number ( @{ $self->getVATNumbers } ) { + my $deleteUrl = $url->page('shop=tax;method=do;do=deleteVATNumber;vatNumber='.$number->{ vatNumber }); + $output .= + '' + ; + } + + $output .= '
CountryVAT Number
' + . join( '', + $self->getCountryName( $number->{ countryCode } ), + $number->{ vatNumber }, + $number->{ name }, + $number->{ address }, + $number->{ approved }, + qq{delete}, + ) + . '
'; + + my $f = WebGUI::HTMLForm->new( $self->session ); + $f->hidden( + name => 'shop', + value => 'tax', + ); + $f->hidden( + name => 'method', + value => 'do', + ); + $f->hidden( + name => 'do', + value => 'addVATNumber', + ); + $f->text( + name => 'vatNumber', + label => 'VAT Number', + ); + $f->submit( + value => 'Add', + ); + $output .= $f->print; + + return $output; +} + +#------------------------------------------------------------------- +sub getTaxRate { + my $self = shift; + my $sku = shift; + my $address = shift; + + my $config = $sku->getTaxConfiguration( $self->className ); + + # Fetch the tax group from the sku. If the sku has none, use the default tax group. + my $taxGroupId = $config->{ taxGroup } || $self->get( 'defaultGroup' ); + my $taxRate = $self->getGroupRate( $taxGroupId ); + + # No shipping address yet. Return group tax rate. + return $taxRate unless defined $address; + + # Shipping address outside EU? That means exporting so no VAT. + my $country = $self->getCountryCode( $address->get( 'country' ) ); + return 0 unless defined $country; + + # Shipping address in same country as shop? Pay VAT; + return $taxRate if $country eq $self->get('shopCountry'); + + # Customer has VAT number in shipping country? Exempt from paying VAT. + return 0 if $self->hasVATNumber( $country ); + + # Customer has no VAT number and resides in EU. Pay VAT; + return $taxRate; +} + +#------------------------------------------------------------------- +sub getVATNumbers { + my $self = shift; + my $countryCode = shift; + my $session = $self->session; + + my $sql = 'select * from tax_eu_vatNumbers where userId=?'; + my $placeHolders = [ $session->user->userId ]; + + if ( $countryCode ) { + $sql .= ' and countryCode=?'; + push @{ $placeHolders }, $countryCode; + } + + my $numbers = $session->db->buildArrayRefOfHashRefs( $sql, $placeHolders ); + + return $numbers; +} + +#------------------------------------------------------------------- +sub hasVATNumber { + my $self = shift; + my $countryCode = shift; + + my $numbers = $self->getVATNumbers( $countryCode ); + return 0 unless @{ $numbers }; + + return $numbers->[0]->{ approved }; +} + +#------------------------------------------------------------------- +sub skuFormDefinition { + my $self = shift; + + my $taxGroups = $self->get( 'taxGroups' ); + + # If no tax groups are defined there's no need to add a form element. + return {} unless $taxGroups; + + my %options = + map { $_->{ id } => "$_->{ name } ($_->{ rate } \%)" } + @{ $taxGroups }; + + tie my %definition, 'Tie::IxHash', ( + taxGroup => { + fieldType => 'selectBox', + label => 'Tax group', + options => \%options, + } + ); + + return \%definition; +} + +#------------------------------------------------------------------- +sub www_addGroup { + my $self = shift; + my $form = $self->session->form; + + 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 } ); + + return ''; +} + +#------------------------------------------------------------------- +sub www_addVATNumber { + my $self = shift; + my $session = $self->session; + my ($db, $form) = $session->quick( qw{ db form } ); + + return $session->privilege->insufficient if $session->user->isVisitor; + + 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 '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, + ] ); + + my $instance = WebGUI::Content::Account->createInstance($session,"shop"); + return $instance->displayContent( $instance->callMethod("manageTaxData", [], $session->user->userId) ); +} + +#------------------------------------------------------------------- +sub www_deleteGroup { + my $self = shift; + my $form = $self->session->form; + + 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 } ); + + return ''; +} + +#------------------------------------------------------------------- +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' ), + ] ); + + my $instance = WebGUI::Content::Account->createInstance($session,"shop"); + return $instance->displayContent( $instance->callMethod("manageTaxData", [], $session->user->userId) ); +} + +#------------------------------------------------------------------- +sub www_saveConfiguration { + my $self = shift; + my $form = $self->session->form; + + return $self->session->privilege->insufficient unless $self->canManage; + + $self->update( { + shopCountry => $form->process( 'shopCountry', 'selectBox' ), + } ); + + return ''; +} + +#------------------------------------------------------------------- +sub www_setDefaultGroup { + my $self = shift; + my $form = $self->session->form; + + return $self->session->privilege->insufficient unless $self->canManage; + + $self->update( { + defaultGroup => $form->process( 'groupId' ), + } ); + + return ''; +} + +1; + diff --git a/lib/WebGUI/Shop/TaxDriver/Generic.pm b/lib/WebGUI/Shop/TaxDriver/Generic.pm new file mode 100644 index 000000000..0bf0eb921 --- /dev/null +++ b/lib/WebGUI/Shop/TaxDriver/Generic.pm @@ -0,0 +1,688 @@ +package WebGUI::Shop::TaxDriver::Generic; + +use strict; + +use WebGUI::Text; +use WebGUI::Storage; +use WebGUI::Exception::Shop; +use List::Util qw{ sum }; + +use base qw{ WebGUI::Shop::TaxDriver }; + + +=head1 NAME + +Package WebGUI::Shop::TaxDriver::Generic + +=head1 DESCRIPTION + +This package manages tax information, and calculates taxes on a shopping cart. It isn't a classic object +in that the only data it contains is a WebGUI::Session object, but it does provide several methods for +handling the information in the tax tables. + +Taxes are accumulated through increasingly specific geographic information. For example, you can +specify the sales tax for a whole country, then the additional sales tax for a state in the country, +all the way down to a single code inside of a city. + +=head1 SYNOPSIS + + use WebGUI::Shop::Tax; + + my $tax = WebGUI::Shop::Tax->new($session); + +=head1 METHODS + +These subroutines are available from this package: + +=cut + +#------------------------------------------------------------------- + +=head2 add ( [$params] ) + +Add tax information to the table. Returns the taxId of the newly created tax information. + +=head3 $params + +A hash ref of the geographic and rate information. The country and taxRate parameters +must have defined values. + +=head4 country + +The country this tax information applies to. + +=head4 state + +The state this tax information applies to. state and country together are unique. + +=head4 city + +The ciy this tax information applies to. Cities are unique with state and country information. + +=head4 code + +The postal code this tax information applies to. codes are unique with state and country information. + +=head4 taxRate + +This is the tax rate for the location, as specified by the geographical +fields country, state, city and/or code. The tax rate is stored as +a percentage, like 5.5 . + +=cut + +sub add { + my $self = shift; + my $params = shift; + + WebGUI::Error::InvalidParam->throw(error => 'Must pass in a hashref of params') + unless ref($params) eq 'HASH'; + WebGUI::Error::InvalidParam->throw(error => "Missing required information.", param => 'country') + unless exists($params->{country}) and $params->{country}; + WebGUI::Error::InvalidParam->throw(error => "Missing required information.", param => 'taxRate') + unless exists($params->{taxRate}) and defined $params->{taxRate}; + + $params->{taxId} = 'new'; + my $id = $self->session->db->setRow('tax_generic_rates', 'taxId', $params); + return $id; +} + +#------------------------------------------------------------------- + +=head2 getTaxRate ( sku, address ) + +Returns the tax rate for the given sku with the given shipping address. + +=head3 sku + +An instanciated WebGUI::Asset::Sku object. + +=head3 address + +An instanciated WebGUI::Shop::Address object containing the shipping address for the sku. + +=cut + +sub getTaxRate { + my $self = shift; + my $sku = shift; + my $address = shift; + my $session = $self->session; + my $config = $sku->getTaxConfiguration( $self->className ); + + # Check params + WebGUI::Error::InvalidParam->throw(error => 'Must pass in a WebGUI::Asset::Sku object') + unless $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' ); + + # Check if the sku has a tax rate override, and return that if it has. + if ( $config->{ overrideTaxRate } ) { + return $config->{ taxRateOverride }; + } + + # No tax rate override, so tax is calculated from the tax tables. + + # If no address is supplied yet, return 0% + return 0 unless defined $address; + + # Fetch the taxes for this address and cache it for later use. + my $taxables = $session->stow->get( 'genericTaxables_' . $address->getId ); + unless ($taxables) { + $taxables = $self->getTaxRates($address); + $session->stow->set( 'genericTaxables_' . $address->getId, $taxables ); + } + + # Check for a SKU specific tax override rate + my $itemTax = sum @{ $taxables }; + + return $itemTax; +} + +#------------------------------------------------------------------- +sub className { + return 'WebGUI::Shop::TaxDriver::Generic'; +} + +#------------------------------------------------------------------- + +=head2 delete ( [$params] ) + +Deletes data from the tax table by taxId. + +=head3 $params + +A hashref containing the taxId of the data to delete from the table. + +=head4 taxId + +The taxId of the data to delete from the table. + +=cut + +sub delete { + my $self = shift; + my $params = shift; + WebGUI::Error::InvalidParam->throw(error => 'Must pass in a hashref of params') + unless ref($params) eq 'HASH'; + WebGUI::Error::InvalidParam->throw(error => "Hash ref must contain a taxId key with a defined value") + unless exists($params->{taxId}) and defined $params->{taxId}; + $self->session->db->write('delete from tax_generic_rates where taxId=?', [$params->{taxId}]); + return; +} + +#------------------------------------------------------------------- + +=head2 exportTaxData ( ) + +Creates a tab deliniated file containing all the information from +the tax table. Returns a temporary WebGUI::Storage object containing +the file. The file will be named "siteTaxData.csv". + +=cut + +sub exportTaxData { + my $self = shift; + my $taxIterator = $self->getItems; + my @columns = grep { $_ ne 'taxId' } $taxIterator->getColumnNames; + my $taxData = WebGUI::Text::joinCSV(@columns) . "\n"; + while (my $taxRow = $taxIterator->hashRef() ) { + my @taxData = @{ $taxRow }{@columns}; + foreach my $column (@taxData) { + $column =~ tr/,/|/; ##Convert to the alternation syntax for the text file + } + $taxData .= WebGUI::Text::joinCSV(@taxData) . "\n"; + } + my $storage = WebGUI::Storage->createTemp($self->session); + $storage->addFileFromScalar('siteTaxData.csv', $taxData); + return $storage; +} + +#------------------------------------------------------------------- + +=head2 getAllItems ( ) + +Returns an arrayref of hashrefs, where each hashref is the data for one row of +tax data. taxId is dropped from the dataset. + +=cut + +sub getAllItems { + my $self = shift; + my $taxes = $self->session->db->buildArrayRefOfHashRefs('select country,state,city,code,taxRate from tax_generic_rates order by country, state'); + return $taxes; +} + +#------------------------------------------------------------------- + +=head2 getItems ( ) + +Returns a WebGUI::SQL::Result object for accessing all of the data in the tax table. This +is a convenience method for listing and/or exporting tax data. + +=cut + +sub getItems { + my $self = shift; + my $result = $self->session->db->read('select * from tax_generic_rates order by country, state'); + return $result; +} + +#------------------------------------------------------------------- + +=head2 getTaxRates ( $address ) + +Given a WebGUI::Shop::Address object, return all rates associated with the address as an arrayRef. + +=cut + +sub getTaxRates { + my $self = shift; + my $address = shift; + WebGUI::Error::InvalidObject->throw(error => 'Need an address.', expected=>'WebGUI::Shop::Address', got=>(ref $address)) + unless ref($address) eq 'WebGUI::Shop::Address'; + my $country = $address->get('country'); + my $state = $address->get('state'); + my $city = $address->get('city'); + my $code = $address->get('code'); + my $result = $self->session->db->buildArrayRef( + q{ + select taxRate from tax_generic_rates where find_in_set(?, country) + and (state='' or find_in_set(?, state)) + and (city='' or find_in_set(?, city)) + and (code='' or find_in_set(?, code)) + }, + [ $country, $state, $city, $code, ]); + return $result; +} + +#------------------------------------------------------------------- + +=head2 importTaxData ( $filePath ) + +Import tax information from the specified file in CSV format. The +first line of the file should contain only the name of the columns, in +any order. It may not contain any comments. + +These are the column names, each is required: + +=over 4 + +=item * + +country + +=item * + +state + +=item * + +city + +=item * + +code + +=item * + +taxRate + +=back + +The following lines will contain tax information. Blank +lines and anything following a '#' sign will be ignored from +the second line of the file, on to the end. + +Returns 1 if the import has taken place. This is to help you know +if old data has been deleted and new has been inserted. If an error is +detected, it will throw exceptions. + +=head3 $filePath + +The path to a file with data to import into the Product system. + +=cut + +sub importTaxData { + my $self = shift; + my $filePath = shift; + WebGUI::Error::InvalidParam->throw(error => q{Must provide the path to a file}) + unless $filePath; + WebGUI::Error::InvalidFile->throw(error => qq{File could not be found}, brokenFile => $filePath) + unless -e $filePath; + WebGUI::Error::InvalidFile->throw(error => qq{File is not readable}, brokenFile => $filePath) + unless -r $filePath; + open my $table, '<', $filePath or + WebGUI::Error->throw(error => qq{Unable to open $filePath for reading: $!\n}); + my $headers; + $headers = <$table>; + chomp $headers; + my @headers = WebGUI::Text::splitCSV($headers); + WebGUI::Error::InvalidFile->throw(error => qq{Bad header found in the CSV file}, brokenFile => $filePath) + unless (join(q{-}, sort @headers) eq 'city-code-country-state-taxRate') + and (scalar @headers == 5); + my @taxData = (); + my $line = 1; + while (my $taxRow = <$table>) { + chomp $taxRow; + $taxRow =~ s/\s*#.+$//; + next unless $taxRow; + local $_; + my @taxRow = map { tr/|/,/; $_; } WebGUI::Text::splitCSV($taxRow); + WebGUI::Error::InvalidFile->throw(error => qq{Error found in the CSV file}, brokenFile => $filePath, brokenLine => $line) + unless scalar @taxRow == 5; + push @taxData, [ @taxRow ]; + } + ##Okay, if we got this far, then the data looks fine. + return unless scalar @taxData; + $self->session->db->beginTransaction; + $self->session->db->write('delete from tax_generic_rates'); + foreach my $taxRow (@taxData) { + my %taxRow; + @taxRow{ @headers } = @{ $taxRow }; ##Must correspond 1:1, or else... + $self->add(\%taxRow); + } + $self->session->db->commit; + return 1; +} + +#------------------------------------------------------------------- +sub skuFormDefinition { + my $self = shift; + my $i18n = WebGUI::International->new( $self->session, 'Tax' ); + + tie my %definition, 'Tie::IxHash', ( + overrideTaxRate => { + fieldType => "yesNo", + defaultValue => 0, + label => $i18n->get("override tax rate"), + hoverHelp => $i18n->get("override tax rate help") + }, + taxRateOverride => { + fieldType => "float", + defaultValue => 0.00, + label => $i18n->get("tax rate override"), + hoverHelp => $i18n->get("tax rate override help") + }, + ); + + return \%definition; +} + +#------------------------------------------------------------------- + +=head2 www_deleteTax ( ) + +Delete a row of tax information, using the form variable taxId as +the id of the row to delete. + +=cut + +sub www_deleteTax { + my $self = shift; + my $session = $self->session; + + return $session->privilege->insufficient unless $self->canManage; + + my $taxId = $session->form->get('taxId'); + $self->delete({ taxId => $taxId }); + + return ''; +} + +#------------------------------------------------------------------- + +=head2 www_addTax ( ) + +Add new tax information into the database, via the UI. + +=cut + +sub www_addTax { + my $self = shift; + my $session = $self->session; + + return $session->privilege->insufficient unless $self->canManage; + + my $params; + my ($form) = $session->quick('form'); + $params->{country} = $form->get('country', 'text'); + $params->{state} = $form->get('state', 'text'); + $params->{city} = $form->get('city', 'text'); + $params->{code} = $form->get('code', 'text'); + $params->{taxRate} = $form->get('taxRate', 'float'); + $self->add($params); + + return ''; +} + +#------------------------------------------------------------------- + +=head2 www_exportTax ( ) + +Export the entire tax table as a CSV file the user can download. + +=cut + +sub www_exportTax { + my $self = shift; + my $session = $self->session; + + return $session->privilege->insufficient unless $self->canManage; + + my $storage = $self->exportTaxData(); + $self->session->http->setRedirect($storage->getUrl($storage->getFiles->[0])); + return "redirect"; +} + +#------------------------------------------------------------------- + +=head2 www_getTaxesAsJson ( ) + +Servers side pagination for tax data that is sent as JSON back to the browser to be +displayed in a YUI DataTable. + +=cut + +sub www_getTaxesAsJson { + my ($self) = @_; + my $session = $self->session; + + return $session->privilege->insufficient unless $self->canManage; + + my ($db, $form) = $session->quick(qw(db form)); + my $startIndex = $form->get('startIndex') || 0; + my $numberOfResults = $form->get('results') || 25; + my %goodKeys = qw/country 1 state 1 city 1 code 1 'tax rate' 1/; + my $sortKey = $form->get('sortKey'); + $sortKey = $goodKeys{$sortKey} == 1 ? $sortKey : 'country'; + my $sortDir = $form->get('sortDir'); + $sortDir = lc($sortDir) eq 'desc' ? 'desc' : 'asc'; + my @placeholders = (); + my $sql = 'select SQL_CALC_FOUND_ROWS * from tax_generic_rates'; + my $keywords = $form->get("keywords"); + if ($keywords ne "") { + $db->buildSearchQuery(\$sql, \@placeholders, $keywords, [qw{country state city code}]) + } + push(@placeholders, $startIndex, $numberOfResults); + $sql .= sprintf (" order by %s limit ?,?","$sortKey $sortDir"); + my %results = (); + my @records = (); + my $sth = $db->read($sql, \@placeholders); + while (my $record = $sth->hashRef) { + push(@records,$record); + } + $results{'recordsReturned'} = $sth->rows()+0; + $sth->finish; + $results{'records'} = \@records; + $results{'totalRecords'} = $db->quickScalar('select found_rows()')+0; ##Convert to numeric + $results{'startIndex'} = $startIndex; + $results{'sort'} = undef; + $results{'dir'} = $sortDir; + $session->http->setMimeType('application/json'); + return JSON::to_json(\%results); +} + +#------------------------------------------------------------------- + +=head2 www_importTax ( ) + +Import new tax data from a file provided by the user. This will replace the current +data with the new data. + +=cut + +sub www_importTax { + my $self = shift; + my $session = $self->session; + + return $session->privilege->insufficient unless $self->canManage; + + my $storage = WebGUI::Storage->create($session); + my $taxFile = $storage->addFileFromFormPost('importFile', 1); + eval { + $self->importTaxData($storage->getPath($taxFile)) if $taxFile; + }; + my ($exception, $status_message); + if ($exception = Exception::Class->caught('WebGUI::Error::InvalidFile')) { + $status_message = sprintf 'A problem was found with your file: %s', + $exception->error; + if ($exception->brokenLine) { + $status_message .= sprintf ' on line %d', $exception->brokenLine; + } + } + elsif ($exception = Exception::Class->caught()) { + $status_message = sprintf 'A problem happened during the import: %s', $exception->error; + } + + $session->stow->set( 'tax_message', $status_message ); + return ''; +} + +#------------------------------------------------------------------- + +=head2 www_manage ( $status_message ) + +User interface to manage taxes. Provides a list of current taxes, and forms for adding +new tax info, exporting and importing sets of taxes, and deleting individual tax data. + +=head3 $status_message + +A message to display to the user. This is usually a problem that was found during +import. + +=cut + +sub getConfigurationScreen { + my $self = shift; + my $session = $self->session; + my $status_message = $session->stow->get( 'tax_message' ); + + return $session->privilege->insufficient unless $self->canManage; + + ##YUI specific datatable CSS + my ($style, $url) = $session->quick(qw(style url)); + $style->setLink($url->extras('/yui/build/fonts/fonts-min.css'), {rel=>'stylesheet', type=>'text/css'}); + $style->setLink($url->extras('yui/build/datatable/assets/skins/sam/datatable.css'), {rel=>'stylesheet', type => 'text/CSS'}); + $style->setLink($url->extras('yui/build/paginator/assets/skins/sam/paginator.css'), {rel=>'stylesheet', type => 'text/CSS'}); + $style->setScript($url->extras('/yui/build/utilities/utilities.js'), {type=>'text/javascript'}); + $style->setScript($url->extras('yui/build/json/json-min.js'), {type => 'text/javascript'}); + $style->setScript($url->extras('yui/build/paginator/paginator-min.js'), {type => 'text/javascript'}); + $style->setScript($url->extras('yui/build/datasource/datasource-min.js'), {type => 'text/javascript'}); + ##YUI Datatable + $style->setScript($url->extras('yui/build/datatable/datatable-min.js'), {type => 'text/javascript'}); + ##Default CSS + $style->setRawHeadTags(''); + my $i18n=WebGUI::International->new($session, 'Tax'); + + my $exportForm = WebGUI::Form::formHeader($session,{action => $url->page('shop=tax;method=do;do=exportTax')}) + . WebGUI::Form::submit($session,{value=>$i18n->get('export tax','Shop'), extras=>q{style="float: left;"} }) + . WebGUI::Form::formFooter($session); + my $importForm = WebGUI::Form::formHeader($session,{action => $url->page('shop=tax;method=do;do=importTax')}) + . WebGUI::Form::submit($session,{value=>$i18n->get('import tax','Shop'), extras=>q{style="float: left;"} }) + . q{} + . WebGUI::Form::formFooter($session); + + my $addForm = WebGUI::HTMLForm->new($session,action=>$url->page('shop=tax;method=do;do=addTax')); + $addForm->text( + label => $i18n->get('country'), + hoverHelp => $i18n->get('country help'), + name => 'country', + ); + $addForm->text( + label => $i18n->get('state'), + hoverHelp => $i18n->get('state help'), + name => 'state', + ); + $addForm->text( + label => $i18n->get('city'), + hoverHelp => $i18n->get('city help'), + name => 'city', + ); + $addForm->text( + label => $i18n->get('code'), + hoverHelp => $i18n->get('code help'), + name => 'code', + ); + $addForm->float( + label => $i18n->get('tax rate'), + hoverHelp => $i18n->get('tax rate help'), + name => 'taxRate', + ); + $addForm->submit( + value => $i18n->get('add a tax'), + ); + my $output; + if ($status_message) { + $output = < +$status_message + +EOSM + } + + $output .= q| + + +
+ +
+
|.$addForm->print.q|
+
|.$exportForm.$importForm.q|
+
+ + + +|; + + return $output; +} + +1;