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 000000000..e33db5312
Binary files /dev/null and b/docs/upgrades/packages-7.7.4/root_import_account_shop_shop-account-layout.wgpkg differ
diff --git a/lib/WebGUI/Shop/TaxDriver.pm b/lib/WebGUI/Shop/TaxDriver.pm
new file mode 100644
index 000000000..e469c4aec
--- /dev/null
+++ b/lib/WebGUI/Shop/TaxDriver.pm
@@ -0,0 +1,220 @@
+package WebGUI::Shop::TaxDriver;
+
+use strict;
+
+use Class::InsideOut qw{ :std };
+use JSON qw{ from_json to_json };
+
+readonly session => 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{
| Group name | Rate |
};
+ 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{| }
+ . join( ' | ',
+ $group->{ name } . ( $group->{ id } eq $self->get( 'defaultGroup' ) ? '(default)' : '' ),
+ $group->{ rate },
+ qq{delete},
+ qq{Set as default group},
+ )
+ . q{ |
};
+ }
+ $vatGroups .= 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 = <
+
+
+
+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
'
+ . '| Country | VAT Number |
';
+
+ foreach my $number ( @{ $self->getVATNumbers } ) {
+ my $deleteUrl = $url->page('shop=tax;method=do;do=deleteVATNumber;vatNumber='.$number->{ vatNumber });
+ $output .=
+ '| '
+ . join( ' | ',
+ $self->getCountryName( $number->{ countryCode } ),
+ $number->{ vatNumber },
+ $number->{ name },
+ $number->{ address },
+ $number->{ approved },
+ qq{delete},
+ )
+ . ' |
'
+ ;
+ }
+
+ $output .= '
';
+
+ 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;