#!/usr/local/bin/perl
#**************************************************************************
#
# ActinicOrder.pm	- module for common ordering functions among the
#	Actinic scripts
#
# Written by George Menyhert
#
# Copyright (c) Actinic Software Ltd 1998
#
#**************************************************************************

package ActinicOrder;

use strict;

$::STARTSEQUENCE		= -1;								# some phase settings
$::BILLCONTACTPHASE	= 0;
$::SHIPCONTACTPHASE	= 1;
$::SHIPCHARGEPHASE	= 2;
$::TAXCHARGEPHASE		= 3;
$::GENERALPHASE		= 4;
$::PAYMENTPHASE		= 5;
$::COMPLETEPHASE		= 6;
$::RECEIPTPHASE		= 7;
$::PRELIMINARYINFOPHASE	= 8;

$::PAYMENT_ON_ACCOUNT_LOWER = 964;					# payment on account string IDs
$::PAYMENT_ON_ACCOUNT_UPPER = 965;
#
# define the tax location constants
#
$::eTaxAlways			= 0;
$::eTaxByInvoice		= 1;
$::eTaxByDelivery		= 2;

#
# define some ActinicOrder package constants
#
$ActinicOrder::ZERO		= 0;
$ActinicOrder::EXEMPT	= 1;
$ActinicOrder::TAX1		= 2;
$ActinicOrder::TAX2		= 3;
$ActinicOrder::BOTH		= 4;
$ActinicOrder::BOTH 		= $ActinicOrder::BOTH;	# remove compiler warning
$ActinicOrder::PRORATA	= 5;
$ActinicOrder::CUSTOM	= 6;

$ActinicOrder::PERCENTOFFSET = 10000;				# This value is referenced in the shipping and PSP plug-ins - any changes need to be reflected there

$ActinicOrder::TRUNCATION				= 0;
$ActinicOrder::SCIENTIFIC_DOWN		= 1;
$ActinicOrder::SCIENTIFIC_NORMAL		= 2;
$ActinicOrder::CEILING					= 3;

#
# define the tax rounding constants
#
$ActinicOrder::ROUNDPERLINE	= 0;
$ActinicOrder::ROUNDPERITEM	= 1;
$ActinicOrder::ROUNDPERORDER	= 2;

#
# flag to stop multiple parsing of tax data
#
$ActinicOrder::bTaxDataParsed	= $::FALSE;

$ActinicOrder::prog_name = 'ActinicOrder.pm';						# Program Name
$ActinicOrder::prog_name = $ActinicOrder::prog_name;				# remove compiler error
$ActinicOrder::prog_ver = '$Revision: 400 $ ';						# program version
$ActinicOrder::prog_ver = substr($ActinicOrder::prog_ver, 11);	# strip the revision information
$ActinicOrder::prog_ver =~ s/ \$//;										# and the trailers

$ActinicOrder::UNDEFINED_REGION = "UndefinedRegion";
$ActinicOrder::REGION_NOT_SUPPLIED = '---';		# This value is referenced in the shipping and PSP plug-ins - any changes need to be reflected there
$ActinicOrder::sPriceTemplate = '';

#
# Pricing models for components.
#
$ActinicOrder::PRICING_MODEL_STANDARD  = 0;							# Product price only
$ActinicOrder::PRICING_MODEL_COMP      = 1;							# Sum all component prices
$ActinicOrder::PRICING_MODEL_PROD_COMP = 2;							# Add product price and all components

#
# If this switch is 1 then simple attributes are ignored when deciding volume discounts
# It only applies to products without components, just attributes.
# If the switch is 0 then only identical items count.
#
$ActinicOrder::VDSIMILARLINES = 1;					# Volume discount mode

$ActinicOrder::RETAILID = 1;							# The retail schedule ID is 1

#
# Define some constants for the server context
#
$ActinicOrder::FROM_UNKNOWN	= 0;
$ActinicOrder::FROM_CART		= 1;
$ActinicOrder::FROM_CHECKOUT	= 2;
#
# Define a global to hold the context
#
$ActinicOrder::s_nContext = $ActinicOrder::FROM_UNKNOWN;

#######################################################
#
# CallShippingPlugIn - call the shipping plug-in
#
# Params:	0 - pointer to cart list (optional)
#				1 - product sub total (optional)
#
# Returns:	0 - script execution status
#				1 - script execution error if any
#				2 - a reference to the shipping function
#						status hash (see %::s_Ship_nShippingStatus below)
#				3 - a reference to the shipping function
#						error hash (see %::s_Ship_sShippingError below)
#				4 - $::TRUE if the shipping phase is hidden
#				5 - the description of the selected shipping
#						method
#				6 - the shipping total for this order
#				7 - a pointer to the shipping phase variable
#						table where the keys are lists of strings
#						to replace in the HTML and values are
#						the new HTML strings
#				8 - the handling total for this order
#				9 - the handling description
#
#######################################################

sub CallShippingPlugIn
	{
	no strict 'refs';

#? $::g_sShippingPluginCacheCounter++;

	if ($::s_Ship_bRun)									# if the script was already run, just return the cached results
		{
		return
			(
			$::SUCCESS,
			'',
			\%::s_Ship_nShippingStatus,
			\%::s_Ship_sShippingError,
			$::s_Ship_bShipPhaseIsHidden,
			$::s_Ship_sShippingDescription,
			$::s_Ship_nShipCharges,
			\%::s_Ship_ShippingVariables,
			$::s_Ship_nHandlingCharges,
			$::s_Ship_sHandlingDescription
			);
		}

#? ACTINIC::ASSERT($::g_sShippingPluginCacheCounter == 1, "Shipping plug-in is being called repeatedly.", __LINE__, __FILE__);

	my ($pCartList);

	if (defined $_[0])									# if the optional cart list was supplied, use it
		{
		$pCartList = $_[0];
		}
	if (defined $_[1])									# if the product sub-total was supplied, use it
		{
		$::s_Ship_nSubTotal = $_[1];
		}
	#
	# The shipping plug-in expects the following values:
	#
	#	Expects:		%::g_InputHash 						- contains the input parameters (only for validation modes)
	#				   @::s_Ship_sShipProducts       	- list of product IDs
	#				   @::s_Ship_nShipQuantities     	- list of quantities (to match ProductIDs)
	#				   @::s_Ship_nShipPrices         	- list of unit prices (to match ProductIDs)
	#					%::s_Ship_PriceFormatBlob     	- the price format data
	#					$::s_Ship_sOpaqueShipData     	- contains user shipping selection
	#					$::s_sDeliveryCountryCode			- contains shipping address country code
	#					$::s_sDeliveryRegionCode			- contains shipping address region code
	#					$::s_Ship_bDisplayPrices			- flag indicating whether or not the prices are visible
	#					%::s_Ship_OpaqueDataTables			- product opaque data table
	#					$::s_Ship_nSubTotal					- product sub-total
	#
	#	Affects:		$::s_Ship_sOpaqueShipData     	- contains user shipping selection
	#					$::s_Ship_sOpaqueHandleData		- contains user handling selection
	#					%::s_Ship_nShippingStatus     	- hash table containing the return codes for the
	#																	various functions of the script.  Valid keys are:
	#																	ValidatePreliminaryInput, ValidateFinalInput,
	#																	RestoreFinalUI, CalculateShipping,
	#																	IsFinalPhaseHidden, GetShippingDescription, or CalculateHandling.
	#																	Valid values are:
	#																	$::SUCCESS - OK, $::FAILURE - error
	#					%::s_Ship_sShippingError      	- hash table containing the error messages for the various
	#																	functions of the script.  Valid keys are the same as for
	#																	%::s_Ship_sShippingStatus.
	#					%::s_Ship_PreliminaryInfoVariables - hash where the keys are lists of strings
	#						to replace in the HTML and values are the new HTML strings
	#					%::s_Ship_ShippingVariables   	- hash where the keys are lists of strings
	#						to replace in the HTML and values are the new HTML strings
	#					$::s_Ship_bPrelimIsHidden			- $::TRUE if the preliminary information phase is hidden
	#					$::s_Ship_bShipPhaseIsHidden		- $::TRUE if the shipping phase is hidden
	#					$::s_Ship_sShippingDescription 	- the selected shipping method description
	#					$::s_Ship_sHandlingDescription 	- the selected handling method description
	#					$::s_Ship_sShippingCountryName	- the country the customer selected
	#					$::s_Ship_nShipCharges				- the shipping total for this order
	#					$::s_Ship_nHandlingCharges			- the handling total for this order
	#					$::s_Ship_bDisplayExtraCartInformation    			- determine whether the extra cart xml tag should be displayed or not
	#					%::s_Ship_aShippingClassProviderIDs						- provider ids for which the extra shipping xml tag should be displayed 
	#					%::s_Ship_aBasePlusPerProviderIDs						- provider ids for which the extra base plus per reclaiming xml tag should be displayed 
	#
	$::s_Ship_bDisplayPrices = $$::g_pSetupBlob{PRICES_DISPLAYED};
	%::s_Ship_PriceFormatBlob = ();
	%::s_Ship_OpaqueDataTables = ();
	@::s_Ship_sShipProducts = ();						# clear the array
	@::s_Ship_nShipQuantities = ();
	@::s_Ship_nShipPrices = ();
	@::s_Ship_nShipSeparately = ();
	#
	# load the plug-in
	#
	my (@Response) = GetAdvancedShippingScript(ACTINIC::GetPath());
	if ($Response[0] != $::SUCCESS)					# couldn't load the script
		{
		return
			(
			$Response[0],
			$Response[1],
			undef,
			undef,
			undef,
			undef,
			undef,
			undef,
			undef
			);
		}

	my ($sScript) = $Response[2];
	#
	# get the order summary for validation
	#
	my ($Status, $Message);
	if (!defined $pCartList)							# if the cart list was not passed in
		{
		@Response = $::Session->GetCartObject();
		if ($Response[0] != $::SUCCESS)					# general error
			{
			return (@Response);								# error so return empty string
			}
		my $pCartObject = $Response[2];

		$pCartList = $pCartObject->GetCartList();
		}
	#
	# Load DD product list
	#
	@Response = ACTINIC::GetDigitalContent($pCartList, $::TRUE);
	if ($Response[0] == $::FAILURE)
		{
		return (@Response);
		}
	my %hDDLinks = %{$Response[2]};		
	#
	# Shipping and components is a tricky area.  Pass the number of product lines
	# in as a global (ignore components when calculating the quantity)
	#
	my ($pOrderDetail, $pProduct, $nComponentCount);
	$::s_Ship_nTotalQuantity = 0;
	$nComponentCount = 0;
	foreach $pOrderDetail (@$pCartList)
		{
		#
		# Locate the section blob
		#
		my ($sSectionBlobName);
		($Status, $Message, $sSectionBlobName) = ACTINIC::GetSectionBlobName($$pOrderDetail{SID}); # retrieve the blob name
		if ($Status == $::FAILURE)
			{
			return ($Status, $Message);
			}
		#
		# locate this product's object.
		#
		@Response = ACTINIC::GetProduct($$pOrderDetail{"PRODUCT_REFERENCE"}, $sSectionBlobName,
												  ACTINIC::GetPath());	# get this product object
		($Status, $Message, $pProduct) = @Response;
		if ($Status == $::NOTFOUND)						# the item has been removed from the catalog
			{
			next;
			}
		if ($Status != $::SUCCESS)
			{
			return
				(
				$Status,
				$Message,
				undef,
				undef,
				undef,
				undef,
				undef,
				undef,
				undef
				);
			}
		#
		# If DD is ship exempt then nothing to do just take next
		#
		if ($$::g_pSetupBlob{SHIP_EXEMPT_DD} &&
			$hDDLinks{$$pOrderDetail{"PRODUCT_REFERENCE"}})
			{
			next;
			}
		#
		# Add the number of items in the line to the total count
		#
		$::s_Ship_nTotalQuantity += $$pOrderDetail{"QUANTITY"};
		#
		# Calculate effective quantity taking into account identical items in the cart
		#
		my $nEffectiveQuantity = EffectiveCartQuantity($pOrderDetail,$pCartList,\&IdenticalCartLines,undef);
		#
		# Get the pricing model and calculate the price
		#
		my $nPricingModel = $pProduct->{PRICING_MODEL};
		my $sPrice;
		if ($nPricingModel == $ActinicOrder::PRICING_MODEL_COMP)					# price is sum of components?
			{																						# if so then store the product
			$sPrice = 0;																		# but with zero price
			}
		else																						# product price should be included?
			{																						# then calculate it
			$sPrice = ActinicOrder::CalculateSchPrice($pProduct,$nEffectiveQuantity,$ACTINIC::B2B->Get('UserDigest'));
			}	
		#
		# Store the product information for use with the shipping plug in
		#
		push (@::s_Ship_sShipProducts, $$pOrderDetail{"PRODUCT_REFERENCE"});
		push (@::s_Ship_nShipQuantities, $$pOrderDetail{"QUANTITY"});
		push (@::s_Ship_nShipSeparately, $$pProduct{"SHIP_SEPARATELY"});
		push (@::s_Ship_nShipPrices, $sPrice);
		$::s_Ship_OpaqueDataTables{$$pProduct{REFERENCE}} = $$pProduct{OPAQUE_SHIPPING_DATA};
		#
		# Check if there are any variants
		#
		if( $pProduct->{COMPONENTS} )														# For all components of the product
			{
			my %CurrentItem = %$pOrderDetail;											# Current item from the cart
			my $VariantList = GetCartVariantList(\%CurrentItem);

			my (%Component, $pComp);
			my $nIndex = 1;
			foreach $pComp (@{$pProduct->{COMPONENTS}})								# For all components in the product
				{
				@Response = FindComponent($pComp,$VariantList);						# Find what matches the selection
				($Status, %Component) = @Response;
				if ($Status != $::SUCCESS)													# This means a problem - 'not found' is SUCCESS here
					{
					return ($Status,$Component{text});
					}
				if( $Component{quantity} > 0 )											# For those that were found with non-zero quantity
					{																				# Insert them as they were products
					#
					# Calculate the component price before we override the product reference or component identifier
					#
					my $sRef= $Component{code} && $pComp->[4] == 1 ? $Component{code} : $CurrentItem{"PRODUCT_REFERENCE"} . "_" . $nIndex;
					@Response = GetComponentPrice($Component{price},$nEffectiveQuantity,$Component{quantity}, undef, $sRef);
					if ($Response[0] != $::SUCCESS)
						{
						return (@Response);
						}
					if ($nPricingModel == $ActinicOrder::PRICING_MODEL_STANDARD)	# standard pricing model
						{
						$sPrice = 0;															# then component price shouldn't be added
						}
					else																			# if component price counts
						{
						$sPrice = $Response[2];												# the add it
						}				
					#
					# Use the code if not empty, otherwise create a unique identifier
					#
					$sRef= $Component{code} ? $Component{code} : $CurrentItem{"PRODUCT_REFERENCE"} . "_" . $nIndex;
					push (@::s_Ship_sShipProducts, $sRef);
					push (@::s_Ship_nShipQuantities, $$pOrderDetail{"QUANTITY"} * $Component{quantity});
					push (@::s_Ship_nShipPrices, $sPrice);
					$::s_Ship_OpaqueDataTables{$sRef} = $Component{shipping};
					push (@::s_Ship_nShipSeparately, $Component{ShipSeparate});

					$nComponentCount++;
					}
				$nIndex++;
				}
			}
		}
	#
	# Now execute the plug-in
	#
	if(defined $::g_InputHash{DELIVERPOSTALCODE})
		{
		$::g_ShipContact{'POSTALCODE'} = $::g_InputHash{DELIVERPOSTALCODE};
		}
		
	if(defined $::g_InputHash{DELIVERRESIDENTIAL})
		{
		$::g_LocationInfo{'DELIVERRESIDENTIAL'} = $::g_InputHash{DELIVERRESIDENTIAL};
		}

	$::s_Ship_sSSPOpaqueShipData = $::g_ShipInfo{'SSP'};
	$::s_Ship_sOpaqueShipData = $::g_ShipInfo{'ADVANCED'};
	$::s_Ship_sOpaqueHandleData = $::g_ShipInfo{HANDLING};
	$::s_sDeliveryCountryCode = $::g_LocationInfo{DELIVERY_COUNTRY_CODE};
	$::s_sDeliveryRegionCode = $::g_LocationInfo{DELIVERY_REGION_CODE};
	%::s_Ship_PriceFormatBlob = %{$::g_pCatalogBlob};	# the catalog blob can be used for prices since it contains the price fields
	if (eval($sScript) != $::SUCCESS)				# execute the script
		{
		return
			(
			$::FAILURE,
			ACTINIC::GetPhrase(-1, 160, $@),
			undef,
			undef,
			undef,
			undef,
			undef,
			undef,
			undef,
			undef
			);
		}
	if ($@)  												# the eval failed
		{
		return($::FAILURE, ACTINIC::GetPhrase(-1, 160, $@));
		}
	$::g_ShipInfo{'ADVANCED'} = $::s_Ship_sOpaqueShipData;
	$::g_ShipInfo{HANDLING} = $::s_Ship_sOpaqueHandleData;
	$::g_LocationInfo{DELIVERY_COUNTRY_CODE} = $::s_sDeliveryCountryCode;
	$::g_LocationInfo{DELIVERY_REGION_CODE} = $::s_sDeliveryRegionCode;
	$::g_ShipInfo{'SSP'} = $::s_Ship_sSSPOpaqueShipData;

	$::s_Ship_bRun = $::TRUE;							# make sure the script is only run once per session

	return
		(
		$::SUCCESS,
		'',
		\%::s_Ship_nShippingStatus,
		\%::s_Ship_sShippingError,
		$::s_Ship_bShipPhaseIsHidden,
		$::s_Ship_sShippingDescription,
		$::s_Ship_nShipCharges,
		\%::s_Ship_ShippingVariables,
		$::s_Ship_nHandlingCharges,
		$::s_Ship_sHandlingDescription
		);
	}

#######################################################
#
# GetAdvancedShippingScript - read and return the
#	advanced shipping script
#
# Params:	0 - the path
#
# Returns:	0 - status
#				1 - error message (if any)
#				2 - script
#
# Affects:	$s_sShippingScript - the advanced script
#
#######################################################

sub GetAdvancedShippingScript
	{
	if (defined $ActinicOrder::s_sShippingScript)# if it is already in memory,
		{
		return ($::SUCCESS, "", $ActinicOrder::s_sShippingScript); # we are done
		}

	if ($#_ < 0)											# validate params
		{
		return ($::FAILURE, ACTINIC::GetPhrase(-1, 12, 'GetAdvancedShippingScript'), 0, 0);
		}
	my ($sPath) = $_[0];									# grab the path

	my ($sFilename) = $sPath . "ActinicShipping.fil";

	my @Response = ACTINIC::ReadAndVerifyFile($sFilename);
	if ($Response[0] == $::SUCCESS)					# if successful
		{
		$ActinicOrder::s_sShippingScript = $Response[2]; # record the script
		}
	return (@Response);
	}

###############################################################################
#
# Payment method related routines - Begin
#
###############################################################################
#######################################################
#
# GenerateValidPayments - generate the list of valid payment
# 		methods for a given location
#
# Input:		0 - \@arrMethods - the method list reference
#				1 - $bValidateDelivery - require delivery address validation
#
# Returns:	0 - $::SUCCESS or $::FAILURE
#				1 - generated HTML or error message
#
# Author: Zoltan Magyar
#
#######################################################

sub GenerateValidPayments
	{
	my ($parrMethods, $bValidateDelivery) = @_;
	if (!defined $bValidateDelivery)
		{
		$bValidateDelivery = $::FALSE;
		}
	my @arrFullList = @{$$::g_pPaymentList{'ORDER'}};
	my %Lookup = GetPaymentsForLocation($bValidateDelivery);
	my $nMethodID;
	foreach $nMethodID (@arrFullList)
		{
      if ($$::g_pPaymentList{$nMethodID}{ENABLED})		# if the method is enabled
       	{
			#
			# Is there any location dependency?
			#
			if ( $::g_pLocationList->{EXPECT_PAYMENT} &&	# if so look up the allowed payments
				  !$Lookup{$nMethodID})							# check for availability
				{
				next;													# and skip if not available
				}
        	push (@$parrMethods, $nMethodID);				# add to valid payments else
        	}
      }
	#
	# Add default payment method for business customers
	# if it isn't in the list
	#
	my $sDigest = $ACTINIC::B2B->Get('UserDigest');
	if ( $sDigest )
		{
		my @Response = ActinicOrder::GetAccountDefaultPaymentMethod($sDigest);
		if ($Response[0] != $::SUCCESS)
			{
			return(@Response);
			}
		if (!$Lookup{$Response[2]})								# if the method is
			{
			push (@$parrMethods, $Response[2]);
			}
		}
	}

#######################################################
#
# GetPaymentsForLocation - generate the list of valid payment
# 		methods for a given location
#
# Input:		bValidateDelivery - the delivery address should
#						also be validated - default is $::TRUE
#
# Returns:	hash of locations
#
# Author: Zoltan Magyar
#
#######################################################

sub GetPaymentsForLocation
	{
	my $bValidateDelivery = shift @_;
	if (!defined $bValidateDelivery)
		{
		$bValidateDelivery = $::TRUE;
		}
	my $nMethodID;
	my (%Invoice, %Intersection) = ();
	#
	# If no location dependencies then return the full list
	# of enabled methods
	#
	if (!$::g_pLocationList->{EXPECT_PAYMENT} )
		{
		foreach $nMethodID (@{$$::g_pPaymentList{'ORDER'}})
			{
			if ($$::g_pPaymentList{$nMethodID}{ENABLED})
				{
				$Intersection{$nMethodID} = 1
				}
			}
		return(%Intersection);
		}
	#
	# Process invoice address first
	#
	my @arrInvoiceList = @{$::g_pLocationList->{$::g_LocationInfo{INVOICE_COUNTRY_CODE}}->{ALLOWED_PAYMENT}};

	foreach $nMethodID (@arrInvoiceList)
		{
		if ($$::g_pPaymentList{$nMethodID}{ENABLED})
			{
			$Invoice{$nMethodID} = 1;					# add all enabled invoice country to the hash
			}
		}
	#
	# Check delivery address then
	#
	if ($::g_BillContact{'SEPARATE'} && $bValidateDelivery &&
		 $::g_LocationInfo{DELIVERY_COUNTRY_CODE} &&			# if the delivery address is used
		 $::g_LocationInfo{DELIVERY_COUNTRY_CODE} ne '')
		{														# then get its allowed payments
		my @arrDeliveryList = @{$::g_pLocationList->{$::g_LocationInfo{DELIVERY_COUNTRY_CODE}}->{ALLOWED_PAYMENT}};

		foreach $nMethodID (@arrDeliveryList) 		# and build the intersection
			{
			if ( $Invoice{$nMethodID} &&
			 	  $$::g_pPaymentList{$nMethodID}{ENABLED})
				{
				$Intersection{$nMethodID} = 1;
				}
			}
		return(%Intersection);
		}
	#
	# If we are here then only the invoice address is used
	# so return its hash
	#
	return(%Invoice);
	}

#######################################################
#
# GetDefaultPayment - determine the default payment
# 		to display
#
# Input:		0 - use restored (true/false - default true)
#
# Returns:	0 - payment method ID
#
# Author: Zoltan Magyar
#
#######################################################

sub GetDefaultPayment
	{
	my ($bUseRestored) = shift @_;
	if (!defined $bUseRestored)
		{
		$bUseRestored = $::TRUE;
		}
	#
	# At first check for restored value
	#
	if (0 < length $::g_PaymentInfo{METHOD} &&	# if the method is defined here
		 $bUseRestored)
		{
		return($::g_PaymentInfo{METHOD});			# then use it
		}
	#
	# If it is a customer then get its default
	#
	my $sDigest = $ACTINIC::B2B->Get('UserDigest');
	if ( $sDigest )
		{
		my @Response = ActinicOrder::GetAccountDefaultPaymentMethod($sDigest);
		if ($Response[0] == $::SUCCESS)				# if account default is defined
			{
			return($Response[2]);						# then return it
			}
		}
	#
	# If the defult is still not defined then look up
	# the default form the payment blob
	#
	my $nMethodID;
	foreach $nMethodID (@{$$::g_pPaymentList{'ORDER'}})
		{
		if ($$::g_pPaymentList{$nMethodID}{DEFAULT})
			{
			return($nMethodID);
			}
		}
	#
	# If it is still not defined then return the first in the order
	#
	return($::PAYMENT_UNDEFINED);
	}

#######################################################
#
# GeneratePaymentSelection - generate HTML code for
# 		payment selection combo
#
# Returns:	0 - $::SUCCESS or $::FAILURE
#				1 - generated HTML or error message
#
# Author: Zoltan Magyar
#
#######################################################

sub GeneratePaymentSelection
	{
	my $sHTML = ACTINIC::GetPhrase(-1, 1951);		# "<SELECT NAME='PAYMENTMETHOD' SIZE='1'>\n"
	#
	# Check for valid payment methods
	#
	my @arrMethods;
	GenerateValidPayments(\@arrMethods, $::TRUE);
	#
	# Get the count of the valid payment methods
	#
	my $nPaymentCount = @arrMethods;
	if (0 == $nPaymentCount)							# if there isn't valid payment method then
		{														# try to add the default
		my $nDefault = GetDefaultPayment($::FALSE);
		if ($nDefault == $::PAYMENT_UNDEFINED)		# is the default defined?
			{													# no??? how could we get here?
			return($::FAILURE, ACTINIC::GetPhrase(-1, 1955));
			}
		else													# if default is defined then
			{
			push (@arrMethods, $nDefault);			# add default to the list
			$nPaymentCount++;								# and increase the method counter
			}
		}
	if (1 == $nPaymentCount)
		{
		$sHTML = sprintf("<INPUT TYPE='HIDDEN' NAME='PAYMENTMETHOD' VALUE='%s'>%s",
								$arrMethods[0], $$::g_pPaymentList{$arrMethods[0]}{'PROMPT'});
		}
	else
		{
		#
		# Get the default option
		#
		my $nDefault = GetDefaultPayment(@arrMethods);
		#
		# Add all valid method to the select option tag
		#
		my $sSelectLine = ACTINIC::GetPhrase(-1, 1952);	# "<OPTION VALUE='%s'>%s\n"
		my $nMethodID;
		foreach $nMethodID (@arrMethods)
			{
			if ($nMethodID == $nDefault)						# is it the default method
				{														# then make it selected
				$sHTML .= sprintf(ACTINIC::GetPhrase(-1, 1954), $nMethodID, $$::g_pPaymentList{$nMethodID}{'PROMPT'});
				}
			else
				{
				$sHTML .= sprintf($sSelectLine, $nMethodID, $$::g_pPaymentList{$nMethodID}{'PROMPT'});
				}
			}
		$sHTML .= ACTINIC::GetPhrase(-1, 1953);			# "</SELECT>\n"
		}
	return ($::SUCCESS, $sHTML);
	}

#######################################################
#
# PaymentStringToEnum - convert the specified payment
#	string to an enum value
#
# Params:	0 - the payment string
#
# Returns:	0 - the digit of interest (-1 on error)
#
# Affects:	%s_sPaymentTable
#
#######################################################
#   WARNING WARNING WARNING WARNING WARNING WARNING
#######################################################
#
# Any modification of this function's interface or
# basic functionality have to be reflected in the
# PSP plug ins!!!
# 20 Feb 2002 gmenyhert
#
#######################################################
#   WARNING WARNING WARNING WARNING WARNING WARNING
#######################################################

sub PaymentStringToEnum
	{
	my ($sPayment) = @_;

	return($sPayment);
	}

#######################################################
#
# EnumToPaymentString - convert the enum value to a
#			payment string
#
# Params:	0 - the enumeration
#
# Returns:	0 - payment string ('' on error)
#
#######################################################

sub EnumToPaymentString
	{
	my ($ePayment) = @_;

	return($$::g_pPaymentList{$ePayment}{'PROMPT'});
	}

#######################################################
#
# IsAccountSpecificPaymentMethod - return true if the
#  specified payment method is not available for
#  unregistered customers
#
# Returns:	$::TRUE if method is account specified
#
#######################################################

sub IsAccountSpecificPaymentMethod
	{
	my ($ePayment) = @_;

	return($$::g_pPaymentList{$ePayment}{'CUSTOMER_USE_ONLY'});
	}

#######################################################
#
# IsCreditCardAvailable - return true if the
#  credit card payment method is available
#
# Returns:	$::TRUE if method is available
#
#######################################################

sub IsCreditCardAvailable
	{
	#
	# Check the default payment method first
	#
	if (GetDefaultPayment($::FALSE) == $::PAYMENT_CREDIT_CARD)
		{
		return($::TRUE);
		}
	#
	# Check for valid payment methods
	#
	my (@arrMethods, $nMethodID);
	GenerateValidPayments(\@arrMethods);			# get valid payments
	#
	# Check if there is credit card
	#
	foreach $nMethodID (@arrMethods)
		{														# then record error
		if ($nMethodID == $::PAYMENT_CREDIT_CARD)
			{
			return($::TRUE);
			}
		}
	return($::FALSE);
	}

#######################################################
#
# CountUnregisteredCustomerPaymentOptions - return the
#  number of payment options available to unregistered
#  customers
#
# Returns:	the number of payment options available
#
# Expects:	$g_SetupBlob to be defined
#
#######################################################

sub CountUnregisteredCustomerPaymentOptions
	{
	my $nPaymentOptions = 0;

	my $nMethodID;
	foreach $nMethodID (@{$$::g_pPaymentList{'ORDER'}})
		{
      if ($$::g_pPaymentList{$nMethodID}{ENABLED})
       	{
        	$nPaymentOptions++;
        	}
      }

	return ($nPaymentOptions);
	}

#######################################################
#
# GetAccountDefaultPaymentMethod - returns the default payment
#			method for a customer account.
#
# Params:	0 - the user digest
#
# Returns:	0 - status
#				1 - error message
#				2 - payment method enumeration
#
#######################################################

sub GetAccountDefaultPaymentMethod
	{
	my ($sDigest) = @_;
	#
	# Get the buyer
	#
	my ($Status, $Message, $pBuyer) =
		ACTINIC::GetBuyer($sDigest, ACTINIC::GetPath()); # look up the buyer
	if ($Status != $::SUCCESS)
		{
		return ($Status, $Message);
		}

	#
	# Get the account
	#
	my $pAccount;
	($Status, $Message, $pAccount) = ACTINIC::GetCustomerAccount($$pBuyer{AccountID}, ACTINIC::GetPath());
	if ($Status != $::SUCCESS)
		{
		return ($Status, $Message);
		}

	#
	# Return the payment method
	#
	return($::SUCCESS, '', $pAccount->{DefaultPaymentMethod});
	}

##############################################################################################################
#
# Checkout phases - Begin
#
##############################################################################################################

#######################################################
#
# ParseDelimiterStatus - return a list of delimiters
#	for the regions to be deleted and a list of
#	delimiters for regions to be kept
#
# Params:	0 - the phase of interest
#
# Returns:	0 - pointer to a list of delimiters
#					that border regions to be deleted
#				1 - a pointer to a list of delimiters
#					that border regions to be kept
#
# Affects:	@s_DeleteRegions, @s_KeepRegions
#
# Expects:	$g_SetupBlob to be defined
#
#######################################################

sub ParseDelimiterStatus
	{
	if ($#_ != 0)
		{
		ACTINIC::ReportError(ACTINIC::GetPhrase(-1, 12, 'ParseDelimiterStatus'), ACTINIC::GetPath());
		}
	my ($nPhase) = @_;

	undef @ACTINIC::s_DeleteRegions;								# clear static buffers
	undef @ACTINIC::s_KeepRegions;
	#
	# build the delimiter prefix
	#
	my ($sPrefix);
	if ($nPhase == $::BILLCONTACTPHASE)
		{
		$sPrefix = 'INVOICE';
		}
	elsif ($nPhase == $::SHIPCONTACTPHASE)
		{
		$sPrefix = 'DELIVER';
		}
	elsif ($nPhase == $::SHIPCHARGEPHASE)
		{
		$sPrefix = 'SHIP';
		}
	elsif ($nPhase == $::TAXCHARGEPHASE)
		{
		$sPrefix = 'TAX';
		}
	elsif ($nPhase == $::GENERALPHASE)
		{
		$sPrefix = 'GENERAL';
		}
	elsif ($nPhase == $::PAYMENTPHASE)
		{
		$sPrefix = 'PAYMENT';
		}
	elsif ($nPhase == $::COMPLETEPHASE)
		{
		$sPrefix = '';
		}
	elsif ($nPhase == $::RECEIPTPHASE)
		{
		$sPrefix = '';
		}
	elsif ($nPhase == $::PRELIMINARYINFOPHASE)
		{
		$sPrefix = '';
		}
	#
	# Check to see that the prompts are visible.  Start by retrieving the list of phrases for this phase
	#
	my ($pPromptList) = $::g_PhraseIndex{$nPhase};
	#
	# we have our phase of interest
	#
	my $nPhraseID;
	my ($sDelimiter);
	foreach $nPhraseID (@$pPromptList)				# loop over the prompts for this phase
		{
		my ($pBlob) = $$::g_pPromptList{"$nPhase,$nPhraseID"};
		if (!defined $pBlob)								# if this blob is not defined, assume it does not exist
			{
			next;
			}

		$sDelimiter = sprintf('%sPROMPT%3.3d',		# create the delimiter name
			$sPrefix, $nPhraseID);
		#
		# handle special cases where prompts may not be specifically hidden, but are really hidden
		# as side effects of other configuration settings
		#
		if ($nPhase == $::TAXCHARGEPHASE)							# the tax page is special
			{
			if ($nPhraseID == 0)											# if this is the exempt 1 prompt
				{
				if(!defined $$::g_pTaxSetupBlob{TAX_1} ||			# if tax 1 is not charged
					!$$::g_pTaxSetupBlob{TAX_1}{ALLOW_EXEMPT})	# or it isn't exemptable
					{
					$$pBlob{STATUS} = $::HIDDEN;						# hide the prompt
					}
				else
					{
					$$pBlob{STATUS} = $::OPTIONAL;					# show the prompt
					}
				}
			elsif ($nPhraseID == 1)										# if this is the exempt 2 prompt
				{
				if(!defined $$::g_pTaxSetupBlob{TAX_2} ||			# if tax 2 is not charged
					!$$::g_pTaxSetupBlob{TAX_2}{ALLOW_EXEMPT})	# or it isn't exemptable
					{
					$$pBlob{STATUS} = $::HIDDEN;						# hide the prompt
					}
				else
					{
					$$pBlob{STATUS} = $::OPTIONAL;					# show the prompt
					}
				}
			}
		elsif ($nPhase == $::SHIPCHARGEPHASE)		# the shipping charge page is special as well
			{
			if ($nPhraseID == 0 &&						# if this is the shipping total prompt
				 ( !$$::g_pSetupBlob{MAKE_SHIPPING_CHARGE} ||	# and shipping is not charged
				   !$$::g_pSetupBlob{PRICES_DISPLAYED}))	# or the prices are hidden
				{
				$$pBlob{STATUS} = $::HIDDEN;			# hide the prompt
				}
			}
		elsif ($nPhase == $::PAYMENTPHASE)			# the payment page is special as well
			{
			if (!IsCreditCardAvailable() || 			# if CC not accepted
				 $$::g_pSetupBlob{USE_SHARED_SSL} ||# or SharedSSL is used
				 $$::g_pSetupBlob{USE_DH} )			# or java is used
				{
				if ($nPhraseID > 0 &&					# this is a CC prompt
					 $nPhraseID < 6 ||
					 $nPhraseID == 8)
					{
					$$pBlob{STATUS} = $::HIDDEN;		# hide the prompt
					}
				}
			if ($nPhraseID == 0)							# if this is the method prompt,
				{
				#
				# count the payment methods.  if 0 or 1, hide the prompt
				#
				my $nPaymentOptions = CountUnregisteredCustomerPaymentOptions();
				#
				# A customer account may specify a payment method that isn't
				# specified in the setup blob so we may go from one method to two
				#
				my $sDigest = $ACTINIC::B2B->Get('UserDigest');
				if($sDigest ne '')
					{
					#
					# Get the default payment for this account
					#
					my ($nStatus, $sMessage, $nCustomerPaymentOption) = GetAccountDefaultPaymentMethod($sDigest);
					#
					# Is this a customer account only payment method?
					#
					if($nStatus == $::SUCCESS &&
						ActinicOrder::IsAccountSpecificPaymentMethod($nCustomerPaymentOption))
						{
						$nPaymentOptions++;				# increment the count of methods
						}
					}
				if ($nPaymentOptions < 1)
					{
					$$pBlob{STATUS} = $::HIDDEN;		# hide the prompt
					}
				}
			}

		if ($$pBlob{STATUS} == $::HIDDEN)			# if this delimited region is hidden
			{
			push (@ACTINIC::s_DeleteRegions, $sDelimiter);	# add to the delete list
			}
		else													# else the region is left in place
			{
			push (@ACTINIC::s_KeepRegions, $sDelimiter);		# add to the keep list
			}
		}
	return (\@ACTINIC::s_DeleteRegions, \@ACTINIC::s_KeepRegions);
	}

#######################################################
#
# IsPhaseHidden
#
# Params:	0 - the phase of interest
#
# Returns:	0 - $::TRUE if the phase is hidden
#
# Expects:	$g_SetupBlob to be defined
#
#######################################################

sub IsPhaseHidden
	{
	if ($#_ != 0)
		{
		ACTINIC::ReportError(ACTINIC::GetPhrase(-1, 12, 'ParseDelimiterStatus'), ACTINIC::GetPath());
		}
	my ($nPhase) = @_;

	#
	# the shipping plugin uses special checks - handle them first
	#
	my ($bPlugInHidden);
	if ($nPhase == $::SHIPCHARGEPHASE)				# check with the the shipping plug in if we've made it here
		{
		#
		# do advanced shipping validation
		#
		my @Response = CallShippingPlugIn();
		if ($Response[0] == $::SUCCESS &&			# if the script ran and
			 ${$Response[2]}{IsFinalPhaseHidden} == $::SUCCESS) # the phase check was successful and
			{
			if (!$Response[4])							# the control is not hidden
				{
				return ($::FALSE);						# return now, otherwise see if the user defined control is hidden
				}
			if (($Response[6] == 0) &&					# the price is zero
				  $Response[4])							# and the status is hidden
				{
				return($::TRUE);							# do not check further just hide
				}
			}
		}
	elsif ($nPhase == $::PRELIMINARYINFOPHASE)
		{
		#
		# the preliminary information business is hidden if both the invoice and delivery locations
		# are ignored
		#
		return (!$$::g_pLocationList{EXPECT_DELIVERY} && !$$::g_pLocationList{EXPECT_INVOICE})
		}
	elsif ($nPhase == $::TAXCHARGEPHASE)			# the tax page is special
		{
		#
		# A tax is hidden if it isn't defined or there's no exemption
		#
		my $bTax1Hidden = !defined $$::g_pTaxSetupBlob{TAX_1} || !$$::g_pTaxSetupBlob{'TAX_1'}{'ALLOW_EXEMPT'};
		my $bTax2Hidden = !defined $$::g_pTaxSetupBlob{TAX_2} || !$$::g_pTaxSetupBlob{'TAX_2'}{'ALLOW_EXEMPT'};
		#
		# Phase is hidden if both taxes are hidden
		#
		return($bTax1Hidden && $bTax2Hidden);
		}
	#
	# Check to see that the prompts are visible.  Start by retrieving the list of phrases for this phase
	#
	my ($pPromptList) = $::g_PhraseIndex{$nPhase};
	#
	# we have our phase of interest
	#
	my $nPhraseID;
	foreach $nPhraseID (@$pPromptList)				# loop over the prompts for this phase
		{
		my ($pBlob) = $$::g_pPromptList{"$nPhase,$nPhraseID"};
		if (!defined $pBlob)								# if this blob is not defined, assume it is hidden
			{
			next;
			}
		#
		# handle special cases where prompts may not be specifically hidden, but are really hidden
		# as side effects of other configuration settings
		#
		if ($nPhase == $::PAYMENTPHASE)			# the payment page is special as well
			{
			if (!IsCreditCardAvailable() || 			# if CC not accepted
				 $$::g_pSetupBlob{USE_SHARED_SSL} ||# or SharedSSL is used
				 $$::g_pSetupBlob{USE_DH} )			# or java is used
				{
				if ($nPhraseID > 0 &&					# this is a CC prompt
					 $nPhraseID < 6 ||
					 $nPhraseID == 8)
					{
					$$pBlob{STATUS} = $::HIDDEN;		# hide the prompt
					}
				}
			if ($nPhraseID == 0)							# if this is the method prompt,
				{
				#
				# count the payment methods.  if 0 or 1, hide the prompt
				#
				my $nPaymentOptions = CountUnregisteredCustomerPaymentOptions();
				#
				# A customer account may specify a payment method that isn't
				# specified in the setup blob so we may go from one method to two
				#
				my $sDigest = $ACTINIC::B2B->Get('UserDigest');
				if($sDigest ne '')
					{
					#
					# Get the default payment for this account
					#
					my ($nStatus, $sMessage, $nCustomerPaymentOption) = GetAccountDefaultPaymentMethod($sDigest);
					#
					# Is this a customer account only payment method?
					#
					if($nStatus == $::SUCCESS &&
						ActinicOrder::IsAccountSpecificPaymentMethod($nCustomerPaymentOption))
						{
						$nPaymentOptions++;				# increment the count of methods
						}
					}
				if ($nPaymentOptions < 1)
					{
					$$pBlob{STATUS} = $::HIDDEN;		# hide the prompt
					}
				}
			}

		if ($$pBlob{'STATUS'} != $::HIDDEN) 		# if this delimited region is not hidden
			{
			return ($::FALSE);
			}
		}

	return ($::TRUE);
	}

#######################################################
#
# ValidatePreliminaryInfo - validate the preliminary
#	ordering information
#
# Params:	0 - $::TRUE if the data should be validated
#
# Returns:	0 - error message
#
# Expects:	$$::g_pSetupBlob to be defined
#				$::g_LocationInfo to be defined
#				$::g_InputHash to be defined
#
#######################################################

sub ValidatePreliminaryInfo
	{
	if ($#_ != 0)
		{
		return(ACTINIC::GetPhrase(-1, 12, 'ValidatePreliminaryInfo'));
		}
	#
	# If there's no location info, there's nothing to do
	#
	if($$::g_pLocationList{EXPECT_NONE})
		{
		return('');
		}
	my ($bActuallyValidate) = @_;
	#
	# Retrieve and store the location values
	#
	# Get the separate ship flag
	#
	if(defined $::g_InputHash{'LocationInvoiceCountry'} ||
		defined $::g_InputHash{'LocationDeliveryCountry'})
		{
		$::g_LocationInfo{SEPARATESHIP} = $::g_InputHash{'SEPARATESHIP'};
		}
	my $bSeparateShip = $::g_LocationInfo{SEPARATESHIP} ne '';

	$::g_BillContact{'SEPARATE'} = $bSeparateShip;
	my $sOldInvoiceCountry = $::g_LocationInfo{INVOICE_COUNTRY_CODE};
	my $sOldDeliveryCountry = $::g_LocationInfo{DELIVERY_COUNTRY_CODE};
	if(defined $::g_InputHash{'LocationInvoiceCountry'})
		{
		$::g_LocationInfo{INVOICE_COUNTRY_CODE} = $::g_InputHash{'LocationInvoiceCountry'};
		$::g_LocationInfo{INVOICE_REGION_CODE} = $::g_InputHash{'LocationInvoiceRegion'};
		}
	if(defined $::g_InputHash{'LocationDeliveryCountry'})
		{
		$::g_LocationInfo{DELIVERY_COUNTRY_CODE} = $::g_InputHash{'LocationDeliveryCountry'};
		$::g_LocationInfo{DELIVERY_REGION_CODE} = $::g_InputHash{'LocationDeliveryRegion'};
		}
	if(defined $::g_InputHash{'LocationInvoiceCountry'} ||
		defined $::g_InputHash{'LocationDeliveryCountry'})
		{
		$::g_LocationInfo{DELIVERRESIDENTIAL} = $::g_InputHash{'DELIVERRESIDENTIAL'};
		}
	if(defined $::g_InputHash{'DELIVERPOSTALCODE'})
		{
		$::g_LocationInfo{DELIVERPOSTALCODE} = $::g_InputHash{'DELIVERPOSTALCODE'};
		}
	if(defined $::g_InputHash{'INVOICERESIDENTIAL'})
		{
		$::g_LocationInfo{INVOICERESIDENTIAL} = $::g_InputHash{'INVOICERESIDENTIAL'};
		}
	if(defined $::g_InputHash{'INVOICEPOSTALCODE'})
		{
		$::g_LocationInfo{INVOICEPOSTALCODE} = $::g_InputHash{'INVOICEPOSTALCODE'};
		}

	#
	# special case scenario - if both locations are expected, and the delivery location is undefined,
	#	assume the delivery is the same as the invoice
	#
	if ($$::g_pLocationList{EXPECT_BOTH})
	   {
	   if ($::g_LocationInfo{INVOICE_COUNTRY_CODE} &&	# the invoice country is defined, but the delivery
	   	$::g_LocationInfo{DELIVERY_COUNTRY_CODE} eq '')	# country is not
	   	{
	   	#
	   	# They left it as Use Invoice location
	   	#
	   	$::g_LocationInfo{DELIVERY_COUNTRY_CODE} = $::g_LocationInfo{INVOICE_COUNTRY_CODE};
	   	}

	   if ($::g_LocationInfo{INVOICE_COUNTRY_CODE} eq		# the countries are the same,and
	   	 $::g_LocationInfo{DELIVERY_COUNTRY_CODE} &&
	   	 $::g_LocationInfo{INVOICE_REGION_CODE} ne $ActinicOrder::UNDEFINED_REGION && # the invoice state is defined
	   	 $::g_LocationInfo{DELIVERY_REGION_CODE} eq $ActinicOrder::UNDEFINED_REGION)	# but the delivery state is undefined
	   	{
	   	#
	   	# They left it as Use Invoice location
	   	#
	   	$::g_LocationInfo{DELIVERY_REGION_CODE} = $::g_LocationInfo{INVOICE_REGION_CODE};
	   	}
		if(!$bSeparateShip)
			{
			if(defined $$::g_pLocationList{DELIVERPOSTALCODE} && $$::g_pLocationList{DELIVERPOSTALCODE} ne '' &&
				$$::g_pLocationList{DELIVERPOSTALCODE} ne '' &&
				!defined $$::g_pLocationList{INVOICEPOSTALCODE})
				{
				$::g_LocationInfo{INVOICEPOSTALCODE} = $::g_LocationInfo{DELIVERPOSTALCODE};
				}
			#
			# Deal with special cases were only one address uses states/provinces
			#
			if((!defined $$::g_pLocationList{INVOICEADDRESS4} || $$::g_pLocationList{INVOICEADDRESS4}) &&
				defined $$::g_pLocationList{DELIVERADDRESS4} && $$::g_pLocationList{DELIVERADDRESS4})
				{
		   	$::g_LocationInfo{INVOICE_REGION_CODE} = $::g_LocationInfo{DELIVERY_REGION_CODE};
				}

			}
	   }

	if(!$bSeparateShip)										# if we're shipping to the same address
		{
		if($$::g_pLocationList{EXPECT_DELIVERY} &&	# if we're collecting delivery locations
			!$$::g_pLocationList{EXPECT_INVOICE})		# but not invoice locations
			{
			#
			# Copy the invoice location info from delivery
			#
			$::g_LocationInfo{INVOICE_COUNTRY_CODE} = $::g_LocationInfo{DELIVERY_COUNTRY_CODE};
			$::g_LocationInfo{INVOICE_REGION_CODE} = $::g_LocationInfo{DELIVERY_REGION_CODE};
			$::g_LocationInfo{INVOICEPOSTALCODE} = $::g_LocationInfo{DELIVERPOSTALCODE};
			$::g_LocationInfo{INVOICERESIDENTIAL} = $::g_LocationInfo{DELIVERRESIDENTIAL};
			}
		elsif(!$$::g_pLocationList{EXPECT_DELIVERY} &&	# if we're not collecting delivery locations
			$$::g_pLocationList{EXPECT_INVOICE})			# but we're collecting invoice locations
			{
			#
			# Copy the delivery location info from invoice
			#
			$::g_LocationInfo{DELIVERY_COUNTRY_CODE} = $::g_LocationInfo{INVOICE_COUNTRY_CODE};
			$::g_LocationInfo{DELIVERY_REGION_CODE} = $::g_LocationInfo{INVOICE_REGION_CODE};
			$::g_LocationInfo{DELIVERPOSTALCODE} = $::g_LocationInfo{INVOICEPOSTALCODE};
			$::g_LocationInfo{DELIVERRESIDENTIAL} = $::g_LocationInfo{INVOICERESIDENTIAL};
			}
		}
	else
		{
		if($$::g_pLocationList{EXPECT_DELIVERY} &&	# if we're collecting delivery locations
			!$$::g_pLocationList{EXPECT_INVOICE})		# but not invoice locations
			{
			#
			# Clear the invoice location codes
			#
			$::g_LocationInfo{INVOICE_COUNTRY_CODE} = '';
			$::g_LocationInfo{INVOICE_REGION_CODE} = '';
			}
		elsif(!$$::g_pLocationList{EXPECT_DELIVERY} &&	# if we're not collecting delivery locations
			$$::g_pLocationList{EXPECT_INVOICE})			# but we're collecting invoice locations
			{
			#
			# Clear the delivery location codes
			#
			$::g_LocationInfo{DELIVERY_COUNTRY_CODE} = '';
			$::g_LocationInfo{DELIVERY_REGION_CODE} = '';
			}
		}

	#
	# if the locations change, update the address fields
	#
	if ($sOldInvoiceCountry ne $::g_LocationInfo{INVOICE_COUNTRY_CODE})
		{
		#
		# if we are expecting an invoice location reset the invoice country.
		#
		if ($$::g_pLocationList{EXPECT_INVOICE})	# we are expecting an invoice location, so update the invoice country
			{
			if($::g_LocationInfo{INVOICE_COUNTRY_CODE} &&
				$::g_LocationInfo{INVOICE_COUNTRY_CODE} ne $ActinicOrder::REGION_NOT_SUPPLIED)
				{
				$::g_BillContact{'COUNTRY'} = ACTINIC::GetCountryName($::g_LocationInfo{INVOICE_COUNTRY_CODE});
				}
			}
		#
		# if we are not expecting a delivery location, do the same for the shipping address but only if it looks like the that is what was intended (the addresses looked
		# similar to begin with
		#
		if (!$$::g_pLocationList{EXPECT_DELIVERY} &&
			 $::g_ShipContact{COUNTRY} eq ACTINIC::GetCountryName($sOldInvoiceCountry))
			{
			if($::g_LocationInfo{INVOICE_COUNTRY_CODE} &&
				$::g_LocationInfo{INVOICE_COUNTRY_CODE} ne $ActinicOrder::REGION_NOT_SUPPLIED)
				{
				$::g_ShipContact{'COUNTRY'} = ACTINIC::GetCountryName($::g_LocationInfo{INVOICE_COUNTRY_CODE});
				}
			}
		}

	if ($sOldDeliveryCountry ne $::g_LocationInfo{DELIVERY_COUNTRY_CODE})
		{
		#
		# if we are expecting a delivery location, reset the delivery country.
		#
		if ($$::g_pLocationList{EXPECT_DELIVERY})	# we are expecting a delivery location, so update the delivery country
			{
			if($::g_LocationInfo{DELIVERY_COUNTRY_CODE} &&
				$::g_LocationInfo{DELIVERY_COUNTRY_CODE} ne $ActinicOrder::REGION_NOT_SUPPLIED)
				{
				$::g_ShipContact{COUNTRY} = ACTINIC::GetCountryName($::g_LocationInfo{DELIVERY_COUNTRY_CODE});
				}
			}
		#
		# if we are not expecting an invoice location, do the same for the billing address but only if it looks like the that is what was intended (the addresses looked
		# similar to begin with
		#
		if (!$$::g_pLocationList{EXPECT_INVOICE} &&
			 !$bSeparateShip &&
			 $::g_BillContact{COUNTRY} eq ACTINIC::GetCountryName($sOldDeliveryCountry))
			{
			if($::g_LocationInfo{DELIVERY_COUNTRY_CODE} &&
				$::g_LocationInfo{DELIVERY_COUNTRY_CODE} ne $ActinicOrder::REGION_NOT_SUPPLIED)
				{
				$::g_BillContact{'COUNTRY'} = ACTINIC::GetCountryName($::g_LocationInfo{DELIVERY_COUNTRY_CODE});
				}
			}
		}
	#
	# if the destinations are different, make sure the "ship different" flag is checked by default
	#
	if ($$::g_pLocationList{EXPECT_DELIVERY} &&	# both addresses are expected
	    $$::g_pLocationList{EXPECT_INVOICE})
	   {
		if ($::g_LocationInfo{INVOICE_COUNTRY_CODE} ne $::g_LocationInfo{DELIVERY_COUNTRY_CODE} ||
			 $::g_LocationInfo{INVOICE_REGION_CODE} ne $::g_LocationInfo{DELIVERY_REGION_CODE})
			{
#			$::g_BillContact{'SEPARATE'} = $::TRUE;
			}
	   }
	if (($::g_LocationInfo{DELIVERY_COUNTRY_CODE} ne  $::g_LocationInfo{INVOICE_COUNTRY_CODE}) &&	# countries differ
		 ($::g_LocationInfo{INVOICE_COUNTRY_CODE} eq $ActinicOrder::REGION_NOT_SUPPLIED) &&	# and invoice country is 'none of the above'
		!$bSeparateShip)																									# but not separate ship
		{
		$::g_LocationInfo{INVOICE_COUNTRY_CODE} = $::g_LocationInfo{DELIVERY_COUNTRY_CODE};	# invoice to shipping country
		}
	#
	# validate the locations
	#
	my ($sError);
	if (!$bActuallyValidate)
		{
		return ($sError);
		}
	#
	# make sure something is selected, if it was expected
	#
	if ($$::g_pLocationList{EXPECT_BOTH})		# verify the delivery address
		{
		if (($::g_LocationInfo{DELIVERY_COUNTRY_CODE} ne  $::g_LocationInfo{INVOICE_COUNTRY_CODE}) &&	# countries differ
			 ($::g_LocationInfo{INVOICE_COUNTRY_CODE} ne $ActinicOrder::REGION_NOT_SUPPLIED) &&
			!$bSeparateShip)																									# but not separate ship
			{
			$sError .= ACTINIC::GetPhrase(-1, 1974) . ACTINIC::GetPhrase(-1, 1971, $::g_sRequiredColor) . ACTINIC::GetPhrase(-1, 171) .
							ACTINIC::GetPhrase(-1, 1975) . ACTINIC::GetPhrase(-1, 1970) .
							" - ". ACTINIC::GetPhrase(-1, 2068) . "<BR>\n";
			}
		elsif ($::g_LocationInfo{DELIVERY_REGION_CODE} ne  $::g_LocationInfo{INVOICE_REGION_CODE} &&	# countries differ
			(exists $$::g_pLocationList{INVOICEADDRESS4} && $$::g_pLocationList{INVOICEADDRESS4} &&
			exists $$::g_pLocationList{DELIVERADDRESS4} && $$::g_pLocationList{DELIVERADDRESS4}) &&
			!$bSeparateShip)
			{
			$sError .= ACTINIC::GetPhrase(-1, 1974) . ACTINIC::GetPhrase(-1, 1971, $::g_sRequiredColor) . ACTINIC::GetPhrase(-1, 171) .
							ACTINIC::GetPhrase(-1, 1975) . ACTINIC::GetPhrase(-1, 1970) .
							" - ". ACTINIC::GetPhrase(-1, 2069) . "<BR>\n";
			}
		elsif ($::g_LocationInfo{DELIVERY_COUNTRY_CODE} eq  $::g_LocationInfo{INVOICE_COUNTRY_CODE} &&	# countries are the same
			 	 $::g_LocationInfo{INVOICE_COUNTRY_CODE} eq $ActinicOrder::REGION_NOT_SUPPLIED)				# and 'None of the above' is selected
			{
			$sError .= ACTINIC::GetPhrase(-1, 1974) . ACTINIC::GetPhrase(-1, 1971, $::g_sRequiredColor) . ACTINIC::GetPhrase(-1, 171) .
							ACTINIC::GetPhrase(-1, 1975) . ACTINIC::GetPhrase(-1, 1970) .
							" - ". ACTINIC::GetPhrase(-1, 2273) . "<BR>\n";			
			}
		}
	if ($$::g_pLocationList{EXPECT_INVOICE})		# verify the invoice address
		{
		if ($::g_LocationInfo{INVOICE_COUNTRY_CODE} eq '')	# country is undefined
			{
			$sError .= ACTINIC::GetPhrase(-1, 1974) . ACTINIC::GetPhrase(-1, 1971, $::g_sRequiredColor) . ACTINIC::GetPhrase(-1, 191) .
							ACTINIC::GetPhrase(-1, 1975) . ACTINIC::GetPhrase(-1, 1970) .
						 	" - ". ACTINIC::GetPhrase(-1, 195) . "<BR>\n";
			}

		if ($::g_LocationInfo{INVOICE_COUNTRY_CODE} ne '' && # country is defined and
			 $::g_LocationInfo{INVOICE_COUNTRY_CODE} ne $ActinicOrder::REGION_NOT_SUPPLIED && # not the 'None of the above' is selected
			 $::g_LocationInfo{INVOICE_REGION_CODE} ne $ActinicOrder::UNDEFINED_REGION && #  the state is defined and
			 $::g_LocationInfo{INVOICE_REGION_CODE} !~ /^$::g_LocationInfo{INVOICE_COUNTRY_CODE}\./) # the state does not match the country
			{
			$sError .= ACTINIC::GetPhrase(-1, 1974) . ACTINIC::GetPhrase(-1, 1971, $::g_sRequiredColor)  . ACTINIC::GetPhrase(-1, 191) .
							ACTINIC::GetPhrase(-1, 1975) . ACTINIC::GetPhrase(-1, 1970) .
							" - ". ACTINIC::GetPhrase(-1, 196) . "<BR>\n";
			}
		}
	if ($$::g_pLocationList{EXPECT_DELIVERY})		# verify the delivery address
		{
		if ($::g_LocationInfo{DELIVERY_COUNTRY_CODE} eq '')	# country is undefined
			{
			$sError .= ACTINIC::GetPhrase(-1, 1974) . ACTINIC::GetPhrase(-1, 1971, $::g_sRequiredColor) . ACTINIC::GetPhrase(-1, 171) .
							ACTINIC::GetPhrase(-1, 1975) . ACTINIC::GetPhrase(-1, 1970) .
							" - ". ACTINIC::GetPhrase(-1, 195) . "<BR>\n";
			}

		if ($::g_LocationInfo{DELIVERY_COUNTRY_CODE} ne '' && # country is defined and
			 $::g_LocationInfo{DELIVERY_REGION_CODE} ne $ActinicOrder::UNDEFINED_REGION && # the state is defined and
			 $::g_LocationInfo{DELIVERY_REGION_CODE} !~ /^$::g_LocationInfo{DELIVERY_COUNTRY_CODE}\./) # the state does not match the country
			{
			$sError .= ACTINIC::GetPhrase(-1, 1974) . ACTINIC::GetPhrase(-1, 1971, $::g_sRequiredColor) . ACTINIC::GetPhrase(-1, 171) .
							ACTINIC::GetPhrase(-1, 1975) . ACTINIC::GetPhrase(-1, 1970) .
							" - ". ACTINIC::GetPhrase(-1, 196) . "<BR>\n";
			}
		}
	#
	# Check the locations against account addresses
	#
	my $sDigest = $ACTINIC::B2B->Get('UserDigest');
	if($sDigest ne '')
		{
		my ($Status, $sMessage, $pBuyer, $pAccount) = ACTINIC::GetBuyerAndAccount($sDigest, ACTINIC::GetPath());
		if ($Status != $::SUCCESS)
			{
			$sError .= $sMessage;
			}
		#
		# Get the lists of valid addresses
		#
		my ($pAddress, $plistValidAddresses, $plistValidInvoiceAddresses, $plistValidDeliveryAddresses);
		($Status, $sMessage, $plistValidInvoiceAddresses, $plistValidDeliveryAddresses) =
			ACTINIC::GetCustomerAddressLists($pBuyer, $pAccount);
		if ($Status != $::SUCCESS)
			{
			$sError .= $sMessage;
			}
		#
		# Check we have valid account addresses for the locations selected
		#
		# Start with the invoice addresses
		#
		my $sRegion;
		if($#$plistValidInvoiceAddresses == -1 &&
			($pAccount->{InvoiceAddressRule} == 1 || $pBuyer->{InvoiceAddressRule} != 2))
			{
			#
			# Specify the state or country in the error message
			#
			if($::g_LocationInfo{INVOICE_REGION_CODE} eq $ActinicOrder::UNDEFINED_REGION)
				{
				$sRegion = ACTINIC::GetCountryName($::g_LocationInfo{INVOICE_COUNTRY_CODE});
				}
			else
				{
				$sRegion = ACTINIC::GetCountryName($::g_LocationInfo{INVOICE_REGION_CODE});
				}
			$sError .= ACTINIC::GetPhrase(-1, 1949, $sRegion) . "<br>";
			}
		#
		# Now the delivery addresses
		#
		if($#$plistValidDeliveryAddresses == -1 &&
			($pBuyer->{DeliveryAddressRule} != 2))
			{
			#
			# Specify the state or country in the error message
			#
			if($::g_LocationInfo{DELIVERY_REGION_CODE} eq $ActinicOrder::UNDEFINED_REGION)
				{
				$sRegion = ACTINIC::GetCountryName($::g_LocationInfo{DELIVERY_COUNTRY_CODE});
				}
			else
				{
				$sRegion = ACTINIC::GetCountryName($::g_LocationInfo{DELIVERY_REGION_CODE});
				}
			$sError .= ACTINIC::GetPhrase(-1, 1950, $sRegion) . "<br>";
			}
		if($sError eq '')
			{
			my $plistValidTaxableAddresses;
			if ($$::g_pTaxSetupBlob{TAX_BY} != $::eTaxByDelivery)
				{
				$plistValidTaxableAddresses = $plistValidInvoiceAddresses;
				}
			else
				{
				$plistValidTaxableAddresses = $plistValidDeliveryAddresses;
				}
			#
			# Try and set the customer tax exemption data
			#
			SetCustomerTaxExemption($plistValidTaxableAddresses);
			}
		ACTINIC::CloseCustomerAddressIndex(); # The customer index is left open for multiple access, so clean it up here
		}
	#
	# Check the Postal code if this is a required field
	#
	if(exists $$::g_pLocationList{INVOICEPOSTALCODE} &&
		$$::g_pLocationList{INVOICEPOSTALCODE} &&
		$::g_LocationInfo{INVOICEPOSTALCODE} eq '')
		{
		$sError .= ACTINIC::GetRequiredMessage(0, 8);
		}

	if(exists $$::g_pLocationList{DELIVERPOSTALCODE} &&
		$$::g_pLocationList{DELIVERPOSTALCODE} &&
		$::g_LocationInfo{DELIVERPOSTALCODE} eq '')
		{
		$sError .= ACTINIC::GetRequiredMessage(1, 8);
		}
	#
	# only allow the shipping and handling plug to validate the selection if there are no errors to date.
	# otherwise the errors are frequently redundant
	#
	if (!$sError)
		{
		if ($$::g_pSetupBlob{'MAKE_SHIPPING_CHARGE'})
			{
			my ($nStatus, $sMessage, $pCartObject) = $::Session->GetCartObject();
			if($nStatus == $::SUCCESS)
				{
				my @Response = $pCartObject->SummarizeOrder($::FALSE);			# validate the selection
				if ($Response[0] != $::SUCCESS)
					{
					$sMessage = $Response[1];
					}
				else
					{
					@Response = $pCartObject->GetShippingPluginResponse();
					if (${$Response[2]}{ValidatePreliminaryInput} != $::SUCCESS)
						{
						$sMessage = ${$Response[3]}{ValidatePreliminaryInput};
						}
					elsif (${$Response[2]}{ValidateFinalInput} != $::SUCCESS)
						{
						$sMessage = ${$Response[3]}{ValidateFinalInput};
						}
					}
				}
			if($sMessage ne '')
				{
				$sError .= ACTINIC::GetPhrase(-1, 1974) . ACTINIC::GetPhrase(-1, 1971, $::g_sRequiredColor) . ACTINIC::GetPhrase(-1, 171) .
								ACTINIC::GetPhrase(-1, 1975) . ACTINIC::GetPhrase(-1, 1970) .
								" - " . $sMessage . "<BR>\n";
				}
			}
		}

	if ($sError ne "")										# if there are any errors
		{															# indicate the problem phase
		$sError = ACTINIC::GetPhrase(-1, 1974) . ACTINIC::GetPhrase(-1, 1971, $::g_sRequiredColor) . $sError . ACTINIC::GetPhrase(-1, 1975) . ACTINIC::GetPhrase(-1, 1970);
		}

	return ($sError);
	}

#######################################################
#
# SetCustomerTaxExemption - sets the customer tax exemption
#
# Params:	0 - ref to list of valid addresses
#
# Expects:	$$::g_pSetupBlob to be defined
#				$::g_TaxInfo to be defined
#				$::g_InputHash to be defined
#
#######################################################

sub SetCustomerTaxExemption
	{
	my ($plistValidTaxableAddresses) = @_;
	#
	# Check for an empty list
	#
	if($#$plistValidTaxableAddresses == -1)
		{
		return;
		}

	my (%hTax1ID, %hTax2ID, %hTax1Exempt, %hTax2Exempt, %hTax1ExemptionData, %hTax2ExemptionData);
	#
	# Map each of the tax values.  The hash will have a single key if all
	# entries in the list have the same value
	#
	my $pAddress;
	foreach $pAddress (@$plistValidTaxableAddresses)
		{
		$hTax1ID{$pAddress->{Tax1ID}} = 0;
		$hTax1Exempt{$pAddress->{ExemptTax1}} = 0;
		$hTax1ExemptionData{$pAddress->{Tax1ExemptData}} = 0;

		$hTax2ID{$pAddress->{Tax2ID}} = 0;
		$hTax2Exempt{$pAddress->{ExemptTax2}} = 0;
		$hTax2ExemptionData{$pAddress->{Tax2ExemptData}} = 0;
		}

	my($nTaxID, $bTaxExempt, $sTaxExemptionData);
	$pAddress = $plistValidTaxableAddresses->[0];
	#
	# If all tax 1 values match, use that data
	#
	if(keys %hTax1ID == 1 &&
		keys %hTax1Exempt == 1 &&
		keys %hTax1ExemptionData == 1 &&
		$pAddress->{Tax1ID} != -1)
		{
		$::g_TaxInfo{'EXEMPT1'} = $pAddress->{'ExemptTax1'} ? 1 : 0;
		$::g_TaxInfo{'EXEMPT1DATA'} = $pAddress->{'Tax1ExemptData'};
		}
	else
		{
		$::g_TaxInfo{'EXEMPT1'} = 0;
		$::g_TaxInfo{'EXEMPT1DATA'} = '';
		}

	#
	# If all tax 2 values match, use that data
	#
	if(keys %hTax2ID == 1 &&
		keys %hTax2Exempt == 1 &&
		keys %hTax2ExemptionData == 1 &&
		$pAddress->{Tax2ID} != -1)
		{
		$::g_TaxInfo{'EXEMPT2'} = $pAddress->{'ExemptTax2'} ? 1 : 0;
		$::g_TaxInfo{'EXEMPT2DATA'} = $pAddress->{'Tax2ExemptData'};
		}
	else
		{
		$::g_TaxInfo{'EXEMPT2'} = 0;
		$::g_TaxInfo{'EXEMPT2DATA'} = '';
		}

	}

#######################################################
#
# ValidateTax - validate the tax data
#
# Params:	0 - $::TRUE if the data should be validated
# 				1 - flag indicating whether this is
#						an order detail page or a real
#						checkout page.
#						$::TRUE = checkout
#						$::FALSE = order detail
#						default: $::TRUE
#
# Returns:	0 - error message
#
# Expects:	$$::g_pSetupBlob to be defined
#				$::g_TaxInfo to be defined
#				$::g_InputHash to be defined
#
#######################################################

sub ValidateTax
	{
	if ($#_ < 0)
		{
		ACTINIC::ReportError(ACTINIC::GetPhrase(-1, 12, 'ValidateTax'), ACTINIC::GetPath());
		}
	my ($bActuallyValidate) = $_[0];

	my ($bCheckout) = $::TRUE;
	if ($#_ == 1)												# if a flag was sent
		{
		$bCheckout = $_[1];									# use the flag
		}
	my @Response = ParseAdvancedTax();
	if ($Response[0] != $::SUCCESS)
		{
		return($Response[1]);
		}
	my $bTaxAndShipEarly = $$::g_pSetupBlob{'TAX_AND_SHIP_EARLY'};
	my $bNoTaxesEnabled = !(defined $$::g_pTaxSetupBlob{'TAX_1'} || defined $$::g_pTaxSetupBlob{'TAX_2'});
	if ($bNoTaxesEnabled ||
		 $bCheckout)
		{
		$::g_TaxInfo{'DONE'} = $::TRUE;
		}
	#
	# if the phase is done, don't display it
	#
	if (IsPhaseDone($::TAXCHARGEPHASE) ||		# if the tax phase is already complete or
		 !$bCheckout ||
		 $bNoTaxesEnabled)								# no taxes are enabled or
		{
		return('');
		}
	#
	# gather the data
	#
	if(!$::g_InputHash{ADDRESSSELECT} )		# if address select is not defined use input hash, otherwise leave account data untouched
		{
		$::g_TaxInfo{'EXEMPT1'} 	= ($::g_InputHash{'TAXEXEMPT1'} eq "" ? $::FALSE : $::TRUE);
		$::g_TaxInfo{'EXEMPT2'} 	= ($::g_InputHash{'TAXEXEMPT2'} eq "" ? $::FALSE : $::TRUE);
		if(defined $::g_InputHash{'TAXEXEMPT1DATA'})
			{
			$::g_TaxInfo{'EXEMPT1DATA'} 	= $::g_InputHash{'TAXEXEMPT1DATA'};
			}
		if(defined $::g_InputHash{'TAXEXEMPT2DATA'})
			{
			$::g_TaxInfo{'EXEMPT2DATA'} 	= $::g_InputHash{'TAXEXEMPT2DATA'};
			}
		}
	$::g_TaxInfo{'USERDEFINED'} = $::g_InputHash{'TAXUSERDEFINED'};
	#
	# clean up the input
	#
	ACTINIC::TrimHashEntries(\%::g_TaxInfo);
	#
	# validate field input
	#
	my ($sError, $nTax);
	if (!$bActuallyValidate)
		{
		return ($sError);
		}
	#
	# validate the exemption data for the taxes
	#
	foreach $nTax (1 .. 2)
		{
		$sError .= CheckTaxExemption($nTax);
		}
	#
	# validate field requirement status
	#
	if (ACTINIC::IsPromptRequired(3, 2) &&
		$::g_TaxInfo{'USERDEFINED'} eq "")
		{
		$sError .= ACTINIC::GetRequiredMessage(3, 2);
		}

	if ($sError ne "")										# if there are any errors
		{															# indicate the problem phase
		$sError = "<B>" . ACTINIC::GetPhrase(-1, 150) . "</B>" . ACTINIC::GetPhrase(-1, 1961, $sError);
		}
	return ($sError);
	}

#######################################################
#
# IsTaxInfoChanged - check if the TaxInfo hash has been
# 	changed during this session
#
# Params:	nothing
#
# Returns:	0 - $::TRUE if the info changed
#
# Expects:	$::Session to be defined
#				$::g_TaxInfo to be defined
#
# Author: Zoltan Magyar - 3:17 PM 12/19/2002
#
#######################################################

sub IsTaxInfoChanged
	{
	my $sTaxDump = (join /|/, keys %::g_TaxInfo) . (join /|/, values %::g_TaxInfo);
	if ($::g_sTaxDump ne $sTaxDump)
		{
		$::g_sTaxDump = $sTaxDump;
		return $::TRUE;
		}
	return $::FALSE;
	}
	
#######################################################
#
# TaxIsKnown - All the tax info is known?
#
# Params:	nothing
#
# Returns:	0 - $::TRUE if the info is known
#
# Expects:	$::Session to be defined
#				$::g_TaxInfo to be defined
#
# Author: Zoltan Magyar - 1:17 PM 1/31/2003
#
#######################################################

sub TaxIsKnown
	{
	#
	# check if the tax is dependent on the location
	#
	if ($$::g_pTaxSetupBlob{TAX_BY} != $::eTaxAlways)
		{
		#
		# check which address it depends upon
		#
		my $sKeyPrefix = ($$::g_pTaxSetupBlob{TAX_BY} == $::eTaxByInvoice) ?
			'INVOICE_' : 'DELIVERY_';
		#
		# get the location it depends upon
		#
		my ($sTargetCountry, $sTargetRegion);

		$sTargetCountry = $::g_LocationInfo{$sKeyPrefix . 'COUNTRY_CODE'};
		$sTargetRegion = $::g_LocationInfo{$sKeyPrefix . 'REGION_CODE'};
		#
		# Check if we don't know the country yet
		#
		if($sTargetCountry eq '')
			{
			return($::FALSE);
			}	
		}
	return($::g_TaxInfo{'DONE'} == $::TRUE);
	}
	
#######################################################
#
# CheckTaxExemption - check the exemption data is OK
#
# Params:	0 - the tax to check ( 1 or 2)
#
# Returns:	0 - status
#				1 - error message
#
#######################################################

sub CheckTaxExemption
	{
	my ($nTax) = @_;
	my ($sExemptKey, $sExemptDataKey);
	$sExemptKey = 'EXEMPT' . $nTax;
	$sExemptDataKey = $sExemptKey . 'DATA';
	if($::g_pTaxSetupBlob->{MODEL} == 1 &&			# if this is advanced tax
		$::g_TaxInfo{$sExemptKey} &&					# and they have exempted themselves
		defined $::g_TaxInfo{$sExemptDataKey})
		{
		if($::g_TaxInfo{$sExemptDataKey} eq '')	# if they haven't entered anything
			{
			return(ACTINIC::GetPhrase(-1, 298));	# tell them off
			}
		}
	return('');
	}

#######################################################
#
# DisplayPreliminaryInfoPhase - display the preliminary
#	ordering information page
#
# Params:	0 - flag indicating whether this is
#						an order detail page or a real
#						checkout page.
#						$::TRUE = checkout
#						$::FALSE = order detail
#						default: $::TRUE
#
# Returns:	0 - status
#				1 - message
#				2 - pointer to variable table
#				3 - pointer to list of delimited regions
#						to remove
#				4 - pointer to list of unused delimiters
#				5 - pointer to the SELECT hash (keys are
#					<SELECT> list names and values are
#					new default selection)
#
# Affects:	%ActinicOrder::s_VariableTable, @ActinicOrder::s_DeleteDelimiters,
#				@ActinicOrder::s_KeepDelimiters, %ActinicOrder::s_SelectTable
#
#######################################################

sub DisplayPreliminaryInfoPhase
	{
	my ($bCheckout) = $::TRUE;
	if ($#_ == 0)												# if a flag was sent
		{
		$bCheckout = $_[0];									# use the flag
		}

	undef %ActinicOrder::s_VariableTable;
	undef %ActinicOrder::s_SelectTable;
	undef @ActinicOrder::s_DeleteDelimiters;
	undef @ActinicOrder::s_KeepDelimiters;
	#
	# if the phase is done, don't display it
	#
	my $bLocationPageNotApplicable =
		(!$bCheckout &&
		 !$$::g_pSetupBlob{'TAX_AND_SHIP_EARLY'}) || # this is the od page and we are not location early or
		($bCheckout &&															# this is checkout
		 $$::g_pSetupBlob{'TAX_AND_SHIP_EARLY'} &&					# and we're collecting information early
		 $::g_InputHash{ACTION} eq ACTINIC::GetPhrase(-1, 113)); # and this isn't the start page

	if (IsPhaseComplete($::PRELIMINARYINFOPHASE) ||		# if the preliminary info is already completed or
		 $bLocationPageNotApplicable ||
		 !($$::g_pLocationList{EXPECT_DELIVERY} ||	# both locations are ignored
	      $$::g_pLocationList{EXPECT_INVOICE}) ||
		 IsPhaseHidden($::PRELIMINARYINFOPHASE))		# prelim is hidden
		{
		push (@ActinicOrder::s_DeleteDelimiters, 'PRELIMINARYINFORMATION'); # hide the shipping stuff
		return ($::SUCCESS, '', \%ActinicOrder::s_VariableTable, \@ActinicOrder::s_DeleteDelimiters, \@ActinicOrder::s_KeepDelimiters);
		}
	else
		{
		push (@ActinicOrder::s_KeepDelimiters, 'PRELIMINARYINFORMATION');
		}
	#
	# restore the location settings
	#
	if (0 < length $::g_LocationInfo{INVOICE_COUNTRY_CODE})
		{
		$ActinicOrder::s_SelectTable{LocationInvoiceCountry} = $::g_LocationInfo{INVOICE_COUNTRY_CODE};
		}
	if (0 < length $::g_LocationInfo{INVOICE_REGION_CODE})
		{
		$ActinicOrder::s_SelectTable{LocationInvoiceRegion} = $::g_LocationInfo{INVOICE_REGION_CODE};
		}
	if (0 < length $::g_LocationInfo{DELIVERY_COUNTRY_CODE})
		{
		$ActinicOrder::s_SelectTable{LocationDeliveryCountry} = $::g_LocationInfo{DELIVERY_COUNTRY_CODE};
		}
	if (0 < length $::g_LocationInfo{DELIVERY_REGION_CODE})
		{
		$ActinicOrder::s_SelectTable{LocationDeliveryRegion} = $::g_LocationInfo{DELIVERY_REGION_CODE};
		}
	
	$ActinicOrder::s_VariableTable{'NETQUOTEVAR:DELIVERPOSTALCODE'} = $::g_LocationInfo{'DELIVERPOSTALCODE'};
	$ActinicOrder::s_VariableTable{'NETQUOTEVAR:DELIVERRESIDENTIAL'} = $::g_LocationInfo{'DELIVERRESIDENTIAL'} ? 'CHECKED' : '';
	$ActinicOrder::s_VariableTable{'NETQUOTEVAR:INVOICESEPARATECHECKSTATUS'} = $::g_LocationInfo{'SEPARATESHIP'} ? 'CHECKED' : '';
	#
	# upon initialization, the shipping table is empty, so their are no values to substitute.  In this case
	# Catalog skips the section because it thinks it isn't needed.  So ensure their is a variable table entry.
	#
	if ((scalar keys %::ActinicOrder::s_VariableTable) == 0)
		{
		$ActinicOrder::s_VariableTable{"<OPTION VALUE=\"\">"} = "<OPTION VALUE=\"\">";
		}

	return ($::SUCCESS, '', \%ActinicOrder::s_VariableTable, \@ActinicOrder::s_DeleteDelimiters, \@ActinicOrder::s_KeepDelimiters, \%ActinicOrder::s_SelectTable);
	}

#######################################################
#
# DisplayTaxPhase - display the tax info
#	 page
#
# Params:	0 - flag indicating whether this is
#						an order detail page or a real
#						checkout page.
#						$::TRUE = checkout
#						$::FALSE = order detail
#						default: $::TRUE
#
# Returns:	0 - status
#				1 - message
#				2 - pointer to variable table
#				3 - pointer to list of delimited regions
#						to remove
#				4 - pointer to list of unused delimiters
#
# Affects:	%ACTINIC::s_s_VariableTable, @ActinicOrder::s_DeleteDelimiters,
#				@ActinicOrder::s_KeepDelimiters
#
#######################################################

sub DisplayTaxPhase
	{
	my ($bCheckout) = $::TRUE;
	if ($#_ == 0)												# if a flag was sent
		{
		$bCheckout = $_[0];									# use the flag
		}

	undef %ActinicOrder::s_VariableTable;
	undef @ActinicOrder::s_DeleteDelimiters;
	undef @ActinicOrder::s_KeepDelimiters;

	my @Response = ParseAdvancedTax();
	if ($Response[0] != $::SUCCESS)
		{
		return (@Response);
		}

	my $bTaxAndShipEarly = $$::g_pSetupBlob{'TAX_AND_SHIP_EARLY'};
	my $bNoTaxesEnabled = !(defined $$::g_pTaxSetupBlob{'TAX_1'} || defined $$::g_pTaxSetupBlob{'TAX_2'});
	#
	# if the phase is done, don't display it
	#
	if (IsPhaseDone($::TAXCHARGEPHASE) ||			# if the tax phase is already complete or
		 !$bCheckout ||
		 $bNoTaxesEnabled)								# no taxes are enabled
		{
		push (@ActinicOrder::s_DeleteDelimiters, 'TAXPHASE');		# hide the tax stuff
		return ($::SUCCESS, '', \%ActinicOrder::s_VariableTable, \@ActinicOrder::s_DeleteDelimiters, \@ActinicOrder::s_KeepDelimiters);
		}
	else
		{
		push (@ActinicOrder::s_KeepDelimiters, 'TAXPHASE');
		}
	my $sTaxPrefix = $::VARPREFIX.'TAX';
	#
	# restore the default values from the table
	#
	$ActinicOrder::s_VariableTable{$sTaxPrefix.'USERDEFINED'} = ACTINIC::EncodeText2($::g_TaxInfo{'USERDEFINED'});
	#
	# handle exemption check boxes and edit boxes
	#
	my ($nTax, $sExemptFlag, $sExemptData, $sExemptCheckStatus);
	foreach $nTax (1 .. 2)
		{
		my $sTax = "TAX_$nTax";
		if(defined $$::g_pTaxSetupBlob{$sTax})
			{
			$ActinicOrder::s_VariableTable{$sTaxPrefix.$nTax.'DESCRIPTION'} = $$::g_pTaxSetupBlob{$sTax}{NAME};
			$ActinicOrder::s_VariableTable{$sTaxPrefix.$nTax.'MESSAGE'} = $$::g_pTaxSetupBlob{$sTax}{TAX_MSG};
			$ActinicOrder::s_VariableTable{$sTaxPrefix.'PROMPT00'. ($nTax - 1)} = $$::g_pTaxSetupBlob{$sTax}{TAX_EXEMPT_PROMPT};
			}
		#
		# set the name of the keys we'll need
		#
		$sExemptFlag = 'EXEMPT' . $nTax;						# eg EXEMPT1
		$sExemptData = $sExemptFlag . 'DATA';				# eg EXEMPT1DATA
		$sExemptCheckStatus = $sExemptFlag . 'CHECKSTATUS';	# eg EXEMPT1CHECKSTATUS
		#
		# if the user is claiming exemption
		#
		$ActinicOrder::s_VariableTable{$sTaxPrefix.$sExemptData} = $::g_TaxInfo{$sExemptData};
		#
		# restore the exemption check status
		#
		$ActinicOrder::s_VariableTable{$sTaxPrefix.$sExemptCheckStatus} =
			($::g_TaxInfo{$sExemptFlag}) ? 'CHECKED' : '';
		}
	#
	# set a flag for ACTINIC::TemplateString to know it should use the advanced
	# tax HTML template
	#
	if($$::g_pTaxSetupBlob{MODEL} != 0)				# if not simple tax
		{
		$ActinicOrder::s_VariableTable{$::VARPREFIX.'ADVANCEDTAXHTML'} = '1';
		}

	return ($::SUCCESS, '', \%ActinicOrder::s_VariableTable, \@ActinicOrder::s_DeleteDelimiters, \@ActinicOrder::s_KeepDelimiters);
	}

#######################################################
#
# ParseAdvancedTax - Parse the advanced tax data
#
# Affects:	$::g_TaxInfo
#
#######################################################

sub ParseAdvancedTax()
	{
	my $sTaxZone;
	my $pTaxZone;
	#
	# check if the tax is dependent on the location
	#
	if ($$::g_pTaxSetupBlob{TAX_BY} != $::eTaxAlways)
		{
		#
		# check which address it depends upon
		#
		my $sKeyPrefix = ($$::g_pTaxSetupBlob{TAX_BY} == $::eTaxByInvoice) ?
			'INVOICE_' : 'DELIVERY_';
		#
		# get the location it depends upon
		#
		my ($sTargetCountry, $sTargetRegion);

		$sTargetCountry = $::g_LocationInfo{$sKeyPrefix . 'COUNTRY_CODE'};
		$sTargetRegion = $::g_LocationInfo{$sKeyPrefix . 'REGION_CODE'};
		#
		# Check if we don't know the country yet
		#
		if($sTargetCountry eq '')
			{
			return($::SUCCESS, '');
			}
		#
		# build up the key for the zone members table
		#
		my $sZoneMembersKey = ($sTargetRegion eq $ActinicOrder::UNDEFINED_REGION) ?
			"$sTargetCountry.$ActinicOrder::UNDEFINED_REGION" :
			$sTargetRegion;
		#
		# get the list of taxes applied in this zone
		#
		if (defined $$::g_pTaxZoneMembersTable{$sZoneMembersKey})
			{
			$sTaxZone = $$::g_pTaxZoneMembersTable{$sZoneMembersKey};
			}
		#
		# check for an error
		#
		if($sTaxZone eq 'Error')
			{
			my $sErrorMsgFormat;
			if(defined $$::g_pPromptList{"-1,359"}{PROMPT})
				{
				$sErrorMsgFormat = ACTINIC::GetPhrase(-1, 359);
				}
			else
				{
				$sErrorMsgFormat = 'Please select a state/province for %s';
				}
			return($::FAILURE,
				sprintf($sErrorMsgFormat, ACTINIC::GetCountryName($sTargetCountry)));
			}
		$pTaxZone = $$::g_pTaxZonesBlob{$sTaxZone};
		}
	else
		{
		$pTaxZone = $$::g_pTaxZonesBlob{0};
		}

	#
	# save flags for non-exempt buyers
	#
	my ($nTax, $sTaxKey, $sTaxBlobKey);
	my @arrTaxIDs = ($$pTaxZone{TAX_1}, $$pTaxZone{TAX_2});
	my $nTaxIndex = 1;
	foreach $nTax (0..1)
		{
		$sTaxKey = 'TAX_' . $nTaxIndex;
		if(defined $$::g_pTaxSetupBlob{$sTaxKey})	# if this tax is defined in default zone
			{
			if($arrTaxIDs[$nTax] == -1)				# if this tax isn't defined in the current zone
				{
				delete $$::g_pTaxSetupBlob{$sTaxKey};# clear tax out of setup
				}
			elsif($arrTaxIDs[$nTax] != $$::g_pTaxSetupBlob{$sTaxKey}{ID})# a different tax is defined
				{
				#
				# Different tax applies so ditch the old tax for new
				#
				delete $$::g_pTaxSetupBlob{$sTaxKey};
				$$::g_pTaxSetupBlob{$sTaxKey} = $$::g_pTaxesBlob{$arrTaxIDs[$nTax]};
				}
			}
		elsif($arrTaxIDs[$nTax] != -1)				# default tax undefined but current tax defined
			{
			#
			# Plug new tax in
			#
			$$::g_pTaxSetupBlob{$sTaxKey} = $$::g_pTaxesBlob{$arrTaxIDs[$nTax]};
			}
		#
		# Plug the zone prompts into the tax blobs
		#
		if(defined $$::g_pTaxSetupBlob{$sTaxKey})
			{
			SetZoneValues($pTaxZone, $nTaxIndex);
			}

		$nTaxIndex++;										# increment the tax index
		}
	return($::SUCCESS, '');
	}

#######################################################
#
# SetZoneValues - set the zone specific settings into
#	the tax set tax hashes.
#
# Params:	$pTaxZone - tax zone
#				$nTaxIndex - 1 or 2
#
#######################################################

sub SetZoneValues
	{
	my ($pTaxZone, $nIndex) = @_;

	my $sAllowExemptKey = 'ALLOW_TAX_' . $nIndex . '_EXEMPT';
	my $sTaxMsgKey = 'TAX_' . $nIndex . '_MSG';
	my $sTaxPromptKey = 'TAX_' . $nIndex . '_EXEMPT_PROMPT';

	my $pTaxHash = $$::g_pTaxSetupBlob{'TAX_'.$nIndex};

	$$pTaxHash{ALLOW_EXEMPT} = $$pTaxZone{$sAllowExemptKey};
	$$pTaxHash{TAX_MSG} = $$pTaxZone{$sTaxMsgKey};
	$$pTaxHash{TAX_EXEMPT_PROMPT} = $$pTaxZone{$sTaxPromptKey};
	#
	# If exempt is not allowed then remove it from the checkout info
	#
	if (!$$pTaxZone{$sAllowExemptKey})
		{
		delete $::g_TaxInfo{'EXEMPT' . $nIndex . 'DATA'};
		}
	#
	# If it is tax2 then check and update the tax on running total flag
	#
	if($nIndex == 2)
		{
		$$pTaxHash{TAX_ON_TAX} = $$pTaxZone{TAX_ON_TAX};
		#
		# When tax on tax is used then the opaque data also needs to be updated
		#
		my @arrOpaque = split(/=/, $$pTaxHash{TAX_OPAQUE_DATA});	# split the existing opaque data first
		$arrOpaque[3] = $$pTaxZone{TAX_ON_TAX};						# change thee tax on tax flag
		$$pTaxHash{TAX_OPAQUE_DATA} = join "=", @arrOpaque;		# rebuild tax opaque data
		}
	}

#######################################################
#
# IsPhaseComplete - return $::TRUE if the specified
#	phase is marked as being complete.
#
# Params:	0 - phase number
#
# Returns:	0 - $TRUE if complete
#
#######################################################

sub IsPhaseComplete
	{
	if ($#_ != 0)
		{
		return ($::FALSE);
		}
	my ($nPhase) = @_;
	#
	# check the completed status.  Note the hardcoded return values and the ? clauses.  They are in place to guarantee
	# a $TRUE or $FALSE value is returned (instead of "" or undef for false).
	#
	if ($nPhase == $::STARTSEQUENCE)
		{
		return ($::FALSE);
		}
	elsif ($nPhase == $::BILLCONTACTPHASE)
		{
		return ($::g_BillContact{'DONE'} ? $::TRUE : $::FALSE);
		}
	elsif ($nPhase == $::SHIPCONTACTPHASE)
		{
		return ($::g_ShipContact{'DONE'} ? $::TRUE : $::FALSE);
		}
	elsif ($nPhase == $::SHIPCHARGEPHASE)
		{
		return ($::g_ShipInfo{'DONE'} ? $::TRUE : $::FALSE);
		}
	elsif ($nPhase == $::TAXCHARGEPHASE)
		{
		return ($::FALSE);
		}
	elsif ($nPhase == $::GENERALPHASE)
		{
		return ($::g_GeneralInfo{'DONE'} ? $::TRUE : $::FALSE);
		}
	elsif ($nPhase == $::PAYMENTPHASE)
		{
		return ($::g_PaymentInfo{'DONE'} ? $::TRUE : $::FALSE);
		}
	elsif ($nPhase == $::PRELIMINARYINFOPHASE)
		{
		return ($::g_LocationInfo{'DONE'} ? $::TRUE : $::FALSE);
		}
	return ($::FALSE);
	}

#######################################################
#
# IsPhaseDone - return $::TRUE if the specified
#	phase is marked as being complete or hidden.
#
# Params:	0 - phase number
#
# Returns:	0 - $TRUE if complete
#
#######################################################

sub IsPhaseDone
	{
	if ($#_ != 0)
		{
		return ($::FALSE);
		}
	my ($nPhase) = @_;
	return(IsPhaseComplete($nPhase) || IsPhaseHidden($nPhase));
	}

##############################################################################################################
#
# Checkout phases - End
#
##############################################################################################################

##############################################################################################################
#
# Summarize Order - Begin
#
##############################################################################################################

################################################################
#
# DisplayButton - generates HTML button code
#
# Input:		0 - name of the button
#				1 - value (caption) of the button
#				2 - enabled/disabled status
#
# Output:	$sHTML - the HTML of the button
#
# Author: Zoltan Magyar, 9:33 AM 2/12/2002
#
################################################################

sub DisplayButton
	{
	my ($sName, $sValue, $bDisabled) = @_;

	my $sFormat = '<INPUT TYPE="SUBMIT" NAME="%s" VALUE="%s" %s>';

	my $sHTML = sprintf($sFormat,
								ACTINIC::EncodeText2($sName),
								ACTINIC::EncodeText2($sValue),
								$bDisabled ? "DISABLED" : "");
	return $sHTML;
	}

################################################################
#
# PreprocessCartToDisplay - prepare the cart content for HTML
#		display
#
# Input:		0 - a pointer to the cart list
#				1 - plain data (do not encode the results)
#
# Returns:	0 - status
#				1 - error message (if any)
#				2 - array reference of cart
#
# Author: Zoltan Magyar, 4:38 PM 2/27/2002
#
################################################################

sub PreprocessCartToDisplay
	{
	my ($pCartList) = shift;		#$_[0];
	my $bPlain = shift;

	my ($pOrderDetail, %CurrentItem, $pProduct, %hDDLinks);
	my @aCartData;
	my @Response;

	#
	# Get digital content if any
	#
	@Response = ACTINIC::GetDigitalContent($pCartList);
	my $bShowDDLinks = ((scalar (keys %{$Response[2]}) > 0)) ? $::TRUE : $::FALSE;
	@Response = ACTINIC::GetDigitalContent($pCartList, $::TRUE);
	if ($Response[0] == $::FAILURE)
		{
		return (@Response);
		}
	my %hDDLinks = %{$Response[2]};

	foreach $pOrderDetail (@$pCartList)
		{
		my %hLineData;
		my @aComponents;

		%CurrentItem = %$pOrderDetail;				# get the next item
		#
		# Locate the section blob
		#
		my ($Status, $Message, $sSectionBlobName) = ACTINIC::GetSectionBlobName($CurrentItem{SID}); # retrieve the blob name
		if ($Status == $::FAILURE)
			{
			return ($Status, $Message);
			}
		#
		# locate this product's object.
		#
		@Response = ACTINIC::GetProduct($CurrentItem{"PRODUCT_REFERENCE"}, $sSectionBlobName,
												  ACTINIC::GetPath());	# get this product object
		($Status, $Message, $pProduct) = @Response;
		if ($Status == $::FAILURE)
			{
			return (@Response);
			}
		#
		# Calculate effective quantity taking into account identical items in the cart
		#
		my $nEffectiveQuantity = EffectiveCartQuantity($pOrderDetail, $pCartList, \&IdenticalCartLines, undef);

		$hLineData{'NAME'}  		= $$pProduct{'NAME'};
		$hLineData{'REFERENCE'}	= $$pProduct{'REFERENCE'};
		$hLineData{'QUANTITY'}	= $CurrentItem{'QUANTITY'};
		$hLineData{'PRODUCT'}  	= $pProduct;
		#
		# Add link to digital content if required
		#
		if ($hDDLinks{$hLineData{'REFERENCE'}} ne "")	# if digital content exist for this product
			{
			if ($::ReceiptPhase &&								# and this is the receipt
				 $bShowDDLinks)									# and links are allowed
				{														# then add link
				my $nPrompt = $bPlain ? 2251 : 2252;
				$hLineData{'DDLINK'} = ACTINIC::GetPhrase(-1, $nPrompt, $hDDLinks{$hLineData{'REFERENCE'}}[0]);
				}
			if ($$::g_pSetupBlob{"DD_AUTO_SHIP"} &&		# auto ship of DD products?
				$$pProduct{'AUTOSHIP'})							# and this product is selected for autoship
				{														# if so add the shipped quantity
				$hLineData{'SHIPPED'} = $CurrentItem{'QUANTITY'};
				}
			}
		my $ComponentPrice = 0;
		my $nRealIndex = 0;
		#
		# Check if there are any variants
		#
		if( $pProduct->{COMPONENTS} )
			{
			my $VariantList = GetCartVariantList(\%CurrentItem);
			my %Component;
			my $pComponent;
			my $nIndex = 0;
			
			foreach $pComponent (@{$pProduct->{COMPONENTS}})
				{
				@Response = FindComponent($pComponent, $VariantList);
				($Status, %Component) = @Response;
				if ($Status != $::SUCCESS)
					{
					return ($Status, $Component{text});
					}
				#
				# If zero quantity then take next
				#
				if ( $Component{quantity} <= 0 )
					{
					$nIndex++;
					next;
					}
				
				$nIndex++;
				$nRealIndex++;
				
				my %hComponentItem;
				my $nComponentQuantity = 0;
				if ( $pComponent->[$::CBIDX_NAME] )
					{
					$nComponentQuantity = $Component{quantity} * $CurrentItem{'QUANTITY'};
					#
					# If it is the first component and the quantity per product
					# is one then allow quatity in the cart but only if there is 
					# no order line for the main product
					#
					if ($$pProduct{NO_ORDERLINE} &&
						 $nRealIndex == 1 &&
						 $Component{quantity} == 1)
						{
						$hLineData{'CANEDIT'} = 1;
						$hComponentItem{'CANEDIT'} = 1;
						}
					}

				my $sRef= $Component{code} && 
								($pComponent->[$::CBIDX_ASSOCPRODPRICE] == 1 ||
								$Component{'AssociatedPrice'}) ?
								$Component{code} :
								$CurrentItem{"PRODUCT_REFERENCE"} . "_" . $nIndex;
				@Response = GetComponentPrice($Component{price}, $nEffectiveQuantity, 1, undef, $sRef);
				if ($Response[0] != $::SUCCESS)
					{
					return (@Response);
					}
				my $nItemPrice = $Response[2];
				#
				# Calculate retail price
				#
				@Response = GetComponentPrice($Component{price}, $nEffectiveQuantity, 1, $ActinicOrder::RETAILID);
				if ($Response[0] != $::SUCCESS)
					{
					return (@Response);
					}
				my $nRetailPrice = $Response[2];
				#
				# Add to the sum if not separate line
				#
				if (!$pComponent->[$::CBIDX_SEPARATELINE])
					{
					$ComponentPrice += $nItemPrice * $Component{quantity};
					}

				$hLineData{'HASCOMPONENT'} = 1;
				$hComponentItem{'NAME'} 		= $Component{text};
				$hComponentItem{'REFERENCE'} 	= $Component{code};
				$hComponentItem{'QUANTITY'} 	= $nComponentQuantity ? $nComponentQuantity : ($bPlain ? "" : '&nbsp;');
				$hComponentItem{'SEPARATELINE'}	= $pComponent->[$::CBIDX_SEPARATELINE];
				#
				# Store the shipping information
				#
				$hComponentItem{'OPAQUE_SHIPPING_DATA'} = $Component{shipping};
				$hComponentItem{'SHIP_SEPARATELY'} = $Component{ShipSeparate};
				#
				# Add link to digital content if required
				#
				if ($hDDLinks{$Component{code}} ne "")			# if digital content exist for this product
					{
					if ($::ReceiptPhase &&							# and this is the receipt
						 $bShowDDLinks)								# and links are allowed
						{													# then add link
						my $nPrompt = $bPlain ? 2251 : 2252;
						$hComponentItem{'DDLINK'} = ACTINIC::GetPhrase(-1, $nPrompt, $hDDLinks{$Component{code}}[0]);
						}
					if ($$::g_pSetupBlob{"DD_AUTO_SHIP"} &&	# auto ship of DD products?
						$$pProduct{'AUTOSHIP'})						# and this product is selected for autoship
						{													# if so add the shipped quantity
						$hLineData{'SHIPPED'} = $CurrentItem{'QUANTITY'};
						$hComponentItem{'SHIPPED'} = $nComponentQuantity;
						}
					}
				@Response = FormatPrice($nItemPrice, $::TRUE, $::g_pCatalogBlob);

				$hComponentItem{'ACTINICPRICE'} = $nItemPrice;
				$hComponentItem{'ACTINICCOST'} = $nItemPrice  * $nComponentQuantity;

				if ($Response[0] != $::SUCCESS)
					{
					return (@Response);
					}
				if ($bPlain)
					{
					$hComponentItem{'PRICE'} 		= $Response[2];
					}
				else
					{
					@Response = ACTINIC::EncodeText($Response[2],$::TRUE, $::TRUE);
					$hComponentItem{'PRICE'} 		= $Response[1];
					}

				@Response = FormatPrice($nItemPrice  * $nComponentQuantity, $::TRUE, $::g_pCatalogBlob);

				if ($Response[0] != $::SUCCESS)
					{
					return (@Response);
					}
				if ($bPlain)
					{
					$hComponentItem{'COST'} 		= $Response[2];
					}
				else
					{
					@Response = ACTINIC::EncodeText($Response[2],$::TRUE, $::TRUE);
					$hComponentItem{'COST'} 		= $Response[1];
					}
				#
				# See if separate tax should be applied on the component
				#
				my ($nTax1, $nTax2, $sTax1Band, $sTax2Band);
				#
				# If component is separate line then calculate tax on it
				#
				if ($hComponentItem{'SEPARATELINE'})
					{
					my $phashTaxBands = $pProduct;
					my $bTreatCustomAsExempt = $::FALSE;
					#
					# If we have custom tax in either the product or associated product
					# we need the retail price it applies to
					#
					my $nTaxableRetailPrice = $$pProduct{'PRICE'};
					if ($Component{AssociatedTax})	# which tax should be used?
						{										# associated product tax from component blob
						$nTaxableRetailPrice = $nRetailPrice;
						@Response = ActinicOrder::GetProductTaxBands(\%Component);
						#
						# Use the component hash for tax bands
						#
						$phashTaxBands = \%Component;
						}
					else										# use product tax
						{
						@Response = ActinicOrder::GetProductTaxBands($pProduct);
						#
						# If the product has order line and the component is separated
						# and the product tax is custom then the components should be exempt
						#
						if ($pComponent->[$::CBIDX_SEPARATELINE] &&	# the component is separated
							 !$$pProduct{NO_ORDERLINE})					# and the main product has order line
							{
							my ($nBandID, $nPercent, $nFlatRate, $sBandName) = split /=/, $Response[2];
							if ($nBandID == $ActinicOrder::CUSTOM)
								{
								$Response[2] = '1=0=0=';
								}
							($nBandID, $nPercent, $nFlatRate, $sBandName) = split /=/, $Response[3];
							if ($nBandID == $ActinicOrder::CUSTOM)
								{
								$Response[3] = '1=0=0=';
								}
							$bTreatCustomAsExempt = $::TRUE;	# treat custom product tax as exempt
							}
						}
					if ($Response[0] != $::SUCCESS)
						{
						return (@Response);
						}
					#
					# Set the tax opaque data for tax 1
					#
					$sTax1Band = ProductToOrderDetailTaxOpaqueData('TAX_1', $nItemPrice, $Response[2], $nTaxableRetailPrice);
					$sTax2Band = ProductToOrderDetailTaxOpaqueData('TAX_2', $nItemPrice, $Response[3], $nTaxableRetailPrice);

					@Response = CalculateTax($nItemPrice, $CurrentItem{"QUANTITY"} * $Component{quantity}, $Response[2], $Response[3], $nTaxableRetailPrice);
					if ($Response[0] != $::SUCCESS)
						{
						return (@Response);
						}
					#
					# make sure the tax amounts are whole numbers
					#
					$nTax1 = ActinicOrder::RoundScientific($Response[2]);
					$nTax2 = ActinicOrder::RoundScientific($Response[3]);
					#
					# Set the tax opaque data for all taxes
					#
					@Response = ActinicOrder::PrepareProductTaxOpaqueData($phashTaxBands,
						$nItemPrice, $nTaxableRetailPrice, $bTreatCustomAsExempt);
					if($Response[0] != $::SUCCESS)
						{
						return(@Response);
						}
					$hComponentItem{'TAX_OPAQUE_DATA'}		= $Response[2];
					}
				$hComponentItem{'TAXBAND1'}	= $sTax1Band;
				$hComponentItem{'TAXBAND2'}	= $sTax2Band;
				$hComponentItem{'TAX1'}		= $nTax1;
				$hComponentItem{'TAX2'}		= $nTax2;

				push @aComponents, \%hComponentItem;
				}
			}
		#
		# If more than one components are enabled then turn off
		# editable component quantity flag
		#
		if ($hLineData{'CANEDIT'} == 1 &&
			 $nRealIndex > 1)
			{
			$hLineData{'CANEDIT'} = 0;
			}
		
		$hLineData{'COMPONENTS'} = \@aComponents;

		my $sPrice = 0;
		$sPrice = ActinicOrder::CalculateSchPrice($pProduct, $nEffectiveQuantity, $ACTINIC::B2B->Get('UserDigest'));

		my ($nItemTotal);
		my $nPriceModel = $$pProduct{PRICING_MODEL};
		if( $nPriceModel == $ActinicOrder::PRICING_MODEL_PROD_COMP )
			{
			$sPrice += $ComponentPrice;
			}
		elsif( $nPriceModel == $ActinicOrder::PRICING_MODEL_COMP )
			{
			$sPrice = $ComponentPrice;
			}
		if ($sPrice > 0)
			{
			$nItemTotal = $sPrice * $CurrentItem{"QUANTITY"};	# calculate the line sub-total
			#
			# Store plain data
			#
			$hLineData{'ACTINICPRICE'} = $sPrice;
			$hLineData{'ACTINICCOST'} 	= $nItemTotal;
			#
			# Store formatted data
			#
			@Response = FormatPrice($sPrice, $::TRUE, $::g_pCatalogBlob);

			if ($Response[0] != $::SUCCESS)
				{
				return (@Response);
				}
			if ($bPlain)
				{
				$hLineData{'PRICE'} = $Response[2];
				}
			else
				{
				@Response = ACTINIC::EncodeText($Response[2],$::TRUE,$::TRUE);
				$hLineData{'PRICE'} = $Response[1];
				}

			@Response = FormatPrice($nItemTotal, $::TRUE, $::g_pCatalogBlob);

			if ($Response[0] != $::SUCCESS)
				{
				return (@Response);
				}
			if ($bPlain)
				{
				$hLineData{'COST'} = $Response[2];
				}
			else
				{
				@Response = ACTINIC::EncodeText($Response[2],$::TRUE,$::TRUE);
				$hLineData{'COST'} = $Response[1];
				}
			}

		$hLineData{'DATE'} = $CurrentItem{'DATE'};
		$hLineData{'INFO'} = $CurrentItem{'INFOINPUT'};

		my ($nTax1, $nTax2);
		#
		# calculate the taxes on this product
		#
		@Response = GetProductTaxBands($pProduct);
		if ($Response[0] != $::SUCCESS)
			{
			return (@Response);
			}
		#
		# Set the tax opaque data for tax 1 & 2
		#
		$hLineData{'TAXBAND1'} = ProductToOrderDetailTaxOpaqueData('TAX_1', $sPrice, $Response[2], $$pProduct{"PRICE"});
		$hLineData{'TAXBAND2'} = ProductToOrderDetailTaxOpaqueData('TAX_2', $sPrice, $Response[3], $$pProduct{"PRICE"});
		#
		# Calculate tax
		#
		@Response = CalculateTax($sPrice, $CurrentItem{"QUANTITY"}, $Response[2], $Response[3], $$pProduct{"PRICE"}); # calculate tax 1 on this product
		if ($Response[0] != $::SUCCESS)
			{
			return (@Response);
			}
		#
		# make sure the tax amounts are whole numbers
		#
		$hLineData{'TAX1'} = ActinicOrder::RoundScientific($Response[2]);
		$hLineData{'TAX2'} = ActinicOrder::RoundScientific($Response[3]);

		push @aCartData, \%hLineData;
		}

	return ($::SUCCESS, "", @aCartData);
	}

################################################################
#
# ShowCart - display the shopping cart
#
# Input:		[0] - array of failures
#
# Expects:	%::g_InputHash, and %g_SetupBlob
#					should be defined
#
# Output:	($ReturnCode, $Error, $sHTML, $sCartID)
#				if $ReturnCode = $::FAILURE, the operation failed
#					for the reason specified in $Error
#				Otherwise everything is OK
#				$sHTML - the HTML of the order detail page
#				$sCartID - the cart id
#
# Author: Zoltan Magyar, 9:33 AM 2/12/2002
#
################################################################

sub ShowCart
	{
	my (@Response, $Status, $Message);
	my $pFailures = $_[0];
	#
	# Read the cart
	#
	@Response = $::Session->GetCartObject();
	if ($Response[0] != $::SUCCESS &&
		 $Response[0] != $::EOF) # error out
		{
		return (@Response);
		}
	my $pCartObject = $Response[2];

	my $pCartList = $pCartObject->GetCartList();

	my ($sLine, %VariableTable);
	#
	# add the shopping cart items
	#
	@Response = GenerateShoppingCartLines($pCartList, $::TRUE, $pFailures, "SCTemplate.html");
	if ($Response[0] != $::SUCCESS)
		{
		return (@Response);
		}
	$VariableTable{$::VARPREFIX."CARTDISPLAY"} = $Response[2];	# add the body to the var list
	#
	# add "Back" link
	#
	my ($sBack, $sPrevPage, @sTemp);

	$sPrevPage = $::Session->GetLastShopPage();						 	# get the last page visited

	if( ACTINIC::IsCatalogFramed() )
		{
		$sBack = '';
		}
	else
		{
		$sBack = ACTINIC::GetPhrase(-1, 1973) . "<P><A HREF=\"" . $sPrevPage . "\">" . ACTINIC::GetPhrase(-1, 47) . "</A><P>" . ACTINIC::GetPhrase(-1, 1970) . "\n";
		}
	#
	# Add page ID
	#
	$sBack .= '<INPUT TYPE="HIDDEN" NAME="PAGE" VALUE="CART">';		
	$VariableTable{$::VARPREFIX."BACKLINK"} = $sBack;
	#
	# Compose cart management bar
	#
	if ($pCartObject->CountItems() != 0)
		{
		$ACTINIC::B2B->SetXML("UpdateButton", $::TRUE);
		$ACTINIC::B2B->SetXML("CheckoutButton", $::TRUE);
		}
	else
		{
		$ACTINIC::B2B->SetXML("UpdateButtonDisabled", $::TRUE);
		$ACTINIC::B2B->SetXML("CheckoutButtonDisabled", $::TRUE);
		}

	if ( ($ACTINIC::B2B->Get('UserDigest') &&					# if registered customer
	      $$::g_pSetupBlob{'REG_SHOPPING_LIST'} == 1)		# and shopping list enabled for registered
	      ||																# or
			(!$ACTINIC::B2B->Get('UserDigest') &&				# unregistered customer
			$$::g_pSetupBlob{'UNREG_SHOPPING_LIST'} == 1))	# and shopping list enabled for unregistered
		{																	# then include buttons
		$ACTINIC::B2B->SetXML("ShoppingList", $::TRUE);
		if ($pCartObject->CountItems() == 0)
			{
			$ACTINIC::B2B->SetXML("SaveButtonDisabled", $::TRUE);
			}
		else
			{
			$ACTINIC::B2B->SetXML("SaveButton", $::TRUE);
			}
		if ($pCartObject->IsExternalCartFileExist())
			{
			$ACTINIC::B2B->SetXML("RestoreButton", $::TRUE);
			}
		else
			{
			$ACTINIC::B2B->SetXML("RestoreButtonDisabled", $::TRUE);
			}
		}
	$ACTINIC::B2B->SetXML("ContinueButton", $::TRUE);		# "Continue Shopping" is enabled all the time
	
	my ($sPath, $sHTML);
	$sPath = ACTINIC::GetPath();					# get the path to the web site dir

	@Response = ACTINIC::TemplateFile($sPath."SCTemplate.html", \%VariableTable); # make the substitutions
	($Status, $Message, $sHTML) = @Response;
	if ($Status != $::SUCCESS)
		{
		return (@Response);
		}
	if( !$ACTINIC::B2B->Get('UserDigest') )
		{
		@Response = ACTINIC::MakeLinksAbsolute($sHTML, $::g_sWebSiteUrl, $::g_sContentUrl);
		}
	else
		{
		my $sCgiUrl = $::g_sAccountScript;
		$sCgiUrl   .= ($::g_InputHash{SHOP} ? '?SHOP=' . ACTINIC::EncodeText2($::g_InputHash{SHOP}, $::FALSE) . '&': '?');
		$sCgiUrl   .= 'PRODUCTPAGE=';
		@Response = ACTINIC::MakeLinksAbsolute($sHTML, $sCgiUrl, $::Session->GetBaseUrl());
		}
	return (@Response);
	}

#######################################################
#
# InfoLineHTML - generate a product detail line
#	of the shopping cart.
#
# Params:	0 - info prompt value
#				1 - edit box HTML for info value
#				2 - XML template to be used
#
# Returns:	0 - HTML
#
#######################################################

sub InfoLineHTML
	{
	my ($sPrompt, $sInfo, $sTemplate) = @_;
	#
	# Create variables for substitution
	#
	my %hVariables;
	$hVariables{$::VARPREFIX . 'PROMPTLABEL'}	= $sPrompt;
	$hVariables{$::VARPREFIX . 'PROMPTVALUE'} 	= $sInfo;

	my ($Status, $Message, $sLine) = ACTINIC::TemplateString($sTemplate, \%hVariables); # make the substitutions
	if ($Status != $::SUCCESS)
		{
		return ($Message);
		}
	return($sLine);
	}

#######################################################
#
# DiscountInfoLineHTML - generate a discount info line
#	of the shopping cart.
#
# Params:	0 - discount info
#				1 - template
#
# Returns:	0 - HTML
#
#######################################################

sub DiscountInfoLineHTML
	{
	my ($sInfo, $sTemplate) = @_;
	#
	# Create variables for substitution
	#
	my %hVariables;
	$hVariables{$::VARPREFIX . 'INFOLINE'} 		= $sInfo;

	my ($Status, $Message, $sLine) = ACTINIC::TemplateString($sTemplate, \%hVariables); # make the substitutions
	if ($Status != $::SUCCESS)
		{
		return ($Message);
		}
	return($sLine);
	}
	
#######################################################
#
# DuplicateLinkLineHTML - generate a product duplicate
#	link line of the shopping cart.
#
# Params:	0 - duplicate link
#				1 - template
#
# Returns:	0 - HTML
#
#######################################################

sub DuplicateLinkLineHTML
	{
	my ($sLink, $sTemplate) = @_;
	#
	# Create variables for substitution
	#
	my %hVariables;
	$hVariables{$::VARPREFIX . 'DUPLICATELINK'} = $sLink;

	my ($Status, $Message, $sLine) = ACTINIC::TemplateString($sTemplate, \%hVariables); # make the substitutions
	if ($Status != $::SUCCESS)
		{
		return ($Message);
		}
	return($sLine);
	}
		
#######################################################
#
# ProductLineHTML - generate a product detail line
#	of the shopping cart.
#
# Params:	0 - product reference if any
#				1 - product name
#				2 - quantity
#				3 - template
#
# Returns:	0 - HTML
#
#######################################################

sub ProductLineHTML
	{
	my ($sProdref, $sName, $sQuantity, $sTemplate, $sDuplicateTemplate, $pDuplicates, $sThumbnail, $bIncludeButtons) = @_;
	$sProdref	= $sProdref ? $sProdref : '&nbsp;';
	#
	# Create variables for substitution
	#
	my %hVariables;
	$hVariables{$::VARPREFIX . 'PRODREF'} 		= $sProdref;
	$hVariables{$::VARPREFIX . 'PRODUCTNAME'} = $sName;
	$hVariables{$::VARPREFIX . 'QUANTITY'} 	= $sQuantity;
	$hVariables{$::VARPREFIX . 'DUPLICATELINKCAPTION'} = ACTINIC::GetPhrase(-1, 2376);
	#
	# Check thumbnail
	#
	if (length $sThumbnail > 0)
		{
		my $sWidth  = $$::g_pSetupBlob{SEARCH_THUMBNAIL_WIDTH}  == 0 ? "" : sprintf("width=%d ",  $$::g_pSetupBlob{SEARCH_THUMBNAIL_WIDTH});
		my $sHeight = $$::g_pSetupBlob{SEARCH_THUMBNAIL_HEIGHT} == 0 ? "" : sprintf("height=%d ", $$::g_pSetupBlob{SEARCH_THUMBNAIL_HEIGHT});
		$hVariables{$::VARPREFIX . 'THUMBNAILSIZE'} = $sWidth . $sHeight;		
		$hVariables{$::VARPREFIX . 'THUMBNAIL'} = $sThumbnail;
		$ACTINIC::B2B->SetXML('THUMBNAIL', $::TRUE);
		}
	else
		{
		$ACTINIC::B2B->SetXML('THUMBNAIL', undef);
		}

	my ($Status, $Message, $sLine) = ACTINIC::TemplateString($sTemplate, \%hVariables); # make the substitutions
	if ($Status != $::SUCCESS)
		{
		return ($Message);
		}
	#
	# See if duplicate product links should be included
	#
	if ($::DISPLAY_PRODUCT_DUPLICATE_LINKS &&		# feature turned on?
	  	 defined $pDuplicates && 						# and we have duplicates
	    keys %{$pDuplicates} > 0 &&
	    $bIncludeButtons &&								# and this is the editable cart
	    $$::g_pSetupBlob{'DISPLAY_DUPLICATE_LINKS'})	# we have the required info in the search index
		{
		my $sDuplicateHTML;
		my $sKey;
		foreach $sKey (keys %{$pDuplicates})
			{
			my $sShop = ($::g_InputHash{SHOP} ? '&SHOP=' . ACTINIC::EncodeText2($::g_InputHash{SHOP}, $::FALSE) : '');
			my $sProdLink = sprintf("<A HREF=\"$::g_sSearchScript?PRODREF=%s&NOLOGIN=1%s\">%s</A>",
					ACTINIC::EncodeText2($sKey), 
					$sShop,
					$$pDuplicates{$sKey});			
			$sDuplicateHTML .= DuplicateLinkLineHTML($sProdLink, $sDuplicateTemplate);
			}
		#
		# Insert the XML tag
		#
		$ACTINIC::B2B->SetXML('DuplicateLinkLine', $sDuplicateHTML);
		$ACTINIC::B2B->SetXML('DuplicateLinks', $::TRUE);	
		}
	else
		{
		$ACTINIC::B2B->SetXML('DuplicateLinks', undef);
		}
	$sLine = ACTINIC::ParseXMLCore($sLine);
	return($sLine);
	}

#######################################################
#
# OrderLineHTML - generate a product detail line
#	of the shopping cart.
#
# Params:	0 - include buttons?
#				1 - product table
#				2 - item price
#				3 - line cost
#				4 - delete col content
#				5 - rowspan for delete col
#
# Returns:	0 - generated HTML
#
#######################################################

sub OrderLineHTML
	{
	my ($bIncludeButtons, $sProdTable, $sPrice, $sCost, $sRemove, $sRowspan, $sTemplate, $sInfoLine, $sDateLine) = @_;
	#
	# Create variables for substitution
	#
	my %hVariables;
	$hVariables{$::VARPREFIX . 'PRICE'} = $sPrice;
	$hVariables{$::VARPREFIX . 'COST'} 	= $sCost;
	$hVariables{$::VARPREFIX . 'REMOVEBUTTON'} 	= $sRemove;
	$hVariables{$::VARPREFIX . 'REMOVEROWSPAN'} 	= $sRowspan;
	#
	# Do the substitution for NETQUOTEVARs
	#
	my ($Status, $Message, $sLine) = ACTINIC::TemplateString($sTemplate, \%hVariables); # make the substitutions
	if ($Status != $::SUCCESS)
		{
		return ($Message);
		}
	#
	# Insert the XML tag
	#
	$ACTINIC::B2B->SetXML('ProductLine', $sProdTable);
	$ACTINIC::B2B->SetXML('InfoLine', $sInfoLine);
	$ACTINIC::B2B->SetXML('DateLine', $sDateLine);
	if ($sRowspan > 0)
		{
		$ACTINIC::B2B->SetXML('RemoveButtonSpan', $::TRUE);
		}
	else
		{
		$ACTINIC::B2B->SetXML('RemoveButtonSpan', undef);
		}		
	$sLine = ACTINIC::ParseXMLCore($sLine);
	return($sLine);
	}

#######################################################
#
# AdjustmentLineHTML - generate an order adjustment line
#	of the shopping cart.
#
# Input:	$sAdjustmentDescription	- adjustment description
#			$nAdjustmentTotal			- adjustment total
#			$sHTMLFormat				- HTML format
#
# Returns:	0 - generated HTML
#
#######################################################

sub AdjustmentLineHTML
	{
	my ($sAdjustmentDescription, $nAdjustmentTotal, $sHTMLFormat) = @_;
	my ($nStatus, $sMessage, $sLine, $sAdjustmentTotal);
	#
	# Create variables for substitution
	#
	my %hVariables;
	$hVariables{$::VARPREFIX . 'ADJUSTMENTCAPTION'} = ACTINIC::EncodeText2($sAdjustmentDescription);
	#
	# Format the price
	#
	($nStatus, $sMessage, $sAdjustmentTotal) = FormatPrice($nAdjustmentTotal, $::TRUE, $::g_pCatalogBlob);
	if ($nStatus != $::SUCCESS)
		{
		return ($sMessage);
		}
	$hVariables{$::VARPREFIX . 'ADJUSTMENT'} = ACTINIC::EncodeText2($sAdjustmentTotal);
	#
	# Do the substitution for NETQUOTEVARs
	#
	($nStatus, $sMessage, $sLine) = ACTINIC::TemplateString($sHTMLFormat, \%hVariables); # make the substitutions
	if ($nStatus != $::SUCCESS)
		{
		return ($sMessage);
		}
	return($sLine);
	}

################################################################
#
# GenerateShoppingCartLines - generate a summary view
#	of the shopping cart.
#
# Params:	0 - a pointer to the cart list
#				1 - if true then editable cart is generated
#				2 - list of failures (used for error highlight)
#				3 - cart template to be used
#
# Returns:	0 - status
#				1 - error message (if any)
#				2 - HTML
#
# Affects:  $ACTINIC::B2B - the cart HTML will be inserted
#
# Author:	Zoltan Magyar
#
# This function generates the view of the shopping cart.
# The view is based on the template passed in as $sTemplate.
# This template is parsed to gain the XML entity tree of
# the template. The template should have <Actinic:XMLTEMPLATE NAME="ShoppingCart">
# XML tag containing the following structure:
#
# 	<Actinic:XMLTEMPLATE NAME="ShoppingCart">
#
#		<Actinic:XMLTEMPLATE NAME="OrderLine">
#
#			<Actinic:XMLTEMPLATE NAME="ProductLine">
# 			</Actinic:XMLTEMPLATE>
#
#			<Actinic:XMLTEMPLATE NAME="InfoLine">
# 			</Actinic:XMLTEMPLATE>
#
#			<Actinic:XMLTEMPLATE NAME="DateLine">
# 			</Actinic:XMLTEMPLATE>
#
# 		</Actinic:XMLTEMPLATE>
#
#		<Actinic:REMOVE TAG="EmptyCartLine">
#		</Actinic:REMOVE>
#
#		<Actinic:REMOVE TAG="SubTotalRow">
#		</Actinic:REMOVE>
#
#		<Actinic:REMOVE TAG="AdjustmentRows">
#			<Actinic:XMLTEMPLATE NAME="AdjustmentRow">
#			</Actinic:XMLTEMPLATE>
#		</Actinic:REMOVE>
#
#		<Actinic:REMOVE TAG="ShippingRow">
#		</Actinic:REMOVE>
#
#		<Actinic:REMOVE TAG="HandlingRow">
#		</Actinic:REMOVE>
#
#		<Actinic:REMOVE TAG="Tax1Row">
#		</Actinic:REMOVE>
#
#		<Actinic:REMOVE TAG="Tax2Row">
#		</Actinic:REMOVE>
#
#		<Actinic:REMOVE TAG="TotalRow">
#		</Actinic:REMOVE>
#
# 	</Actinic:XMLTEMPLATE>
#
# All of the XML tags must be specified.
# Once the XML structure is processed the code substitutes
# the approriate values into the HTML fragments and sets
# the appropriate values of the $ACTINIC::B2B object.
#
# NOTE:
# --------------------------------------------------------------
# This function returns the generated HTML but it is only
# kept by compatibility reasons. The cart display will be
# inserted by the XML parser called from ACTINIC::PrintPage
# using the values form $ACTINIC:B2B.
#
################################################################

sub GenerateShoppingCartLines
	{
	no strict 'refs';
	if ($#_ < 0)
		{
		return ($::FAILURE, ACTINIC::GetPhrase(-1, 12, 'GenerateShoppingCartLines'), 0, 0);
		}
	my ($pCartList) 		= $_[0];
	my $bIncludeButtons 	= $_[1];
	my $aFailures 			= $_[2];
	my $sTemplate 			= $_[3];
	#
	# Read the Cart
	#
	my (@Response, $Status, $Message);
	@Response = $::Session->GetCartObject($::TRUE);
	if ($Response[0] != $::SUCCESS)					# general error
		{
		return (@Response);								# error so return empty string
		}
	my $pCartObject = $Response[2];
	#
	# Summarize the order. This will process any product and order adjustments
	#
	@Response = $pCartObject->SummarizeOrder($::TRUE);# calculate the order total
	if ($Response[0] != $::SUCCESS)
		{
		return (@Response);
		}
	my ($Ignore0, $Ignore1, $nSubTotal, $nShipping, $nTax1, $nTax2, $nTotal, $nShippingTax1, $nShippingTax2, $nHandling, $nHandlingTax1, $nHandlingTax2) = @Response;
	@Response = SummarizeOrderPrintable(@Response);	# get the printable versions of the prices
	if ($Response[0] != $::SUCCESS)
		{
		return (@Response);
		}
	my ($Ignore2, $Ignore3, $sSubTotal, $sShipping, $sHandling, $sTax1, $sTax2, $sTotal) = @Response;
	#
	# Preprocess the cart data
	#
	my @aCartData;
	($Status, $Message, @aCartData) = PreprocessCartToDisplay($pCartList);
	if ($Status != $::SUCCESS)
		{
		return ($Status, $Message);
		}
	my ($sOrderLines);
	#
	# Preprocess the template
	#
	my ($sMessage, $pTree);
	($Status, $sMessage, $pTree) = ACTINIC::PreProcessXMLTemplate(ACTINIC::GetPath() . $sTemplate);
	if ($Status != $::SUCCESS)
		{
		return ($Status, $sMessage);
		}
	my $pXML = new Element({"_CONTENT" => $pTree});	# bless the result to have Element structure
	#
	# Get the template for order and product lines
	#
	if (!$pXML->FindNode("XMLTEMPLATE", "NAME", "ShoppingCart"))
		{
		#
		# If the cart is not required in this template then just return and do not
		# pass back any cart table
		#
		return ($::SUCCESS, "", "");
		}
	my $sOrderLineHTML 	= ACTINIC_PXML::GetTemplateFragment($pXML, "OrderLine");
	my $sProductLineHTML = ACTINIC_PXML::GetTemplateFragment($pXML, "ProductLine");
	my $sInfoLineHTML		= ACTINIC_PXML::GetTemplateFragment($pXML, "InfoLine");
	my $sDateLineHTML		= ACTINIC_PXML::GetTemplateFragment($pXML, "DateLine");
	my $sDiscountInfoLineHTML		= ACTINIC_PXML::GetTemplateFragment($pXML, "DiscountInfoLine");
	my $sOrderAdjustmentRowHTML 	= ACTINIC_PXML::GetTemplateFragment($pXML, "AdjustmentRow");
	my $sDuplicateLinkLineHTML 	= ACTINIC_PXML::GetTemplateFragment($pXML, "DuplicateLinkLine");
	#
	# Check for alternate display option
	#
	my $sProductAdjustmentLines;
	my ($bAdjustmentsAtEnd, $sDummy) = ACTINIC::IsCustomVarDefined("ACT_SHOW_ADJUSTMENTS_BOTTOM");	
	#
	# now process the list
	#
	my ($pOrderDetail, %CurrentItem, $pProduct, $nLineCount);
	
	$nLineCount = 0;

	foreach $pOrderDetail (@aCartData)
		{
		%CurrentItem = %$pOrderDetail;				# get the next item
		#
		# Preprocess components
		#
		my @aComponentsIncluded;
		my @aComponentsSeparated;
		my $pComponent;
		foreach $pComponent (@{$CurrentItem{'COMPONENTS'}})
			{
			if ($pComponent->{'SEPARATELINE'})		# component displayed in separate order line
				{
				push @aComponentsSeparated, $pComponent;
				}
			else												# component included in product's order line
				{
				push @aComponentsIncluded, $pComponent;
				}
			}
		#
		# locate this product's object.
		#
		$pProduct = $CurrentItem{'PRODUCT'};
		#
		# Check if product doesn't require orderline
		#
		my $bProductSupressed = $$pProduct{NO_ORDERLINE};
		#
		# Calculate effective quantity taking into account identical items in the cart
		#
		my $nEffectiveQuantity = EffectiveCartQuantity($pOrderDetail, $pCartList, \&IdenticalCartLines, undef);

		@Response = ACTINIC::ProcessEscapableText($$pProduct{'NAME'});
		if ($Response[0] != $::SUCCESS)
			{
			return (@Response);
			}

		my $sShop = ($::g_InputHash{SHOP} ? '&SHOP=' . ACTINIC::EncodeText2($::g_InputHash{SHOP}, $::FALSE) : '');
		my $sProdLinkFormat = "<A HREF=\"$::g_sSearchScript?PRODREF=%s&NOLOGIN=1$sShop\">%s</A>";
		my $sProdLink = !$bIncludeButtons ? $Response[1] : sprintf($sProdLinkFormat, ACTINIC::EncodeText2($$pProduct{'REFERENCE'}), $Response[1]);
		$sProdLink .= $CurrentItem{'DDLINK'};
		#
		# The product quantity is only editable if min&max are different
		#
		my $sQuantityText = $CurrentItem{QUANTITY};

		if ($$pProduct{"MIN_QUANTITY_ORDERABLE"} != $$pProduct{"MAX_QUANTITY_ORDERABLE"}	&& # The quantity might change
			 $bIncludeButtons)
			{
			#
			# Restore old value if validation failed
			#
			if ($aFailures->[$nLineCount]->{"QUANTITY"} &&
				 defined $aFailures->[$nLineCount]->{"BAD_QUANTITY"})
				{
				$sQuantityText = $aFailures->[$nLineCount]->{"BAD_QUANTITY"};
				}
			$sQuantityText = "<INPUT TYPE=TEXT SIZE=\"4\" NAME=\"Q_" . $nLineCount . "\" VALUE=\"". $sQuantityText . "\" STYLE=\"text-align: right;";
			if ($aFailures->[$nLineCount]->{"QUANTITY"})
				{
				$sQuantityText .= "background-color: $::g_sErrorColor";
				}
			$sQuantityText .= "\">";
			}
		elsif ($bIncludeButtons)						# if the quantity is fix, then we have to insert a hidden field for the quantity
			{
			$sQuantityText = "<INPUT TYPE=HIDDEN NAME=\"Q_" . $nLineCount . "\" VALUE=\"". $sQuantityText . "\">" . $sQuantityText;
			}
		#
		# Remove button
		#
		my $sRemove = "<INPUT TYPE=CHECKBOX NAME=\"D_" . $nLineCount . "\">";
		#
		# Date prompt
		#
		my ($sDay, $sMonth, $sYear);
		my $sDateLine;
		if (length $$pProduct{'DATE_PROMPT'} > 0)		# if the date is defined, add it to the table
			{
			if ($aFailures->[$nLineCount]->{"DATE"} &&
				 defined $aFailures->[$nLineCount]->{"DATE"})
				{
				$sDay = substr($aFailures->[$nLineCount]->{"BAD_DATE"}, 8, 2);	# retrieve the date components from the compiled date value
				$sMonth = substr($aFailures->[$nLineCount]->{"BAD_DATE"}, 5, 2); # which is in actinic internal format YYYY/MM/DD
				$sYear = substr($aFailures->[$nLineCount]->{"BAD_DATE"}, 0, 4);
				}
			else
				{
				$sDay = substr($CurrentItem{"DATE"}, 8, 2);	# retrieve the date components from the compiled date value
				$sMonth = substr($CurrentItem{"DATE"}, 5, 2); # which is in actinic internal format YYYY/MM/DD
				$sYear = substr($CurrentItem{"DATE"}, 0, 4);
				}
			#
			# Change date to combo if the cart editable
			#
			$sMonth += 0;									# remove leading zero if any
			$sDay += 0;
			$sMonth = $::g_InverseMonthMap{$sMonth};
			if ($bIncludeButtons)
				{
				my $sStyle;
				if ($aFailures->[$nLineCount]->{"DATE"})
					{
					$sStyle = " style=\"background-color: $::g_sErrorColor\"";
					}
				my $nMinYear = $$pProduct{"DATE_MIN"};
				my $nMaxYear = $$pProduct{"DATE_MAX"};
				$sDay 	= ACTINIC::GenerateComboHTML("DAY_$nLineCount", $sDay, "%2.2d", $sStyle, (1..31));
				$sMonth	= ACTINIC::GenerateComboHTML("M_$nLineCount", $sMonth, "%s", $sStyle, @::gMonthList);
				if ($nMinYear == $nMaxYear)					# if the date range is only one year, the we generate a static text instead of year combo
					{
					$sYear = "$nMinYear<INPUT TYPE=HIDDEN NAME=\"Y_$nLineCount\" VALUE=\"$nMinYear\">"
					}
					else
					{
					$sYear 	= ACTINIC::GenerateComboHTML("Y_$nLineCount", $sYear, "%4.4d", $sStyle, ($nMinYear..$nMaxYear)); # add the year drop down list
					}
				}
			#
			# Format Date as required
			#
			my $sDatePrompt = ACTINIC::FormatDate($sDay, $sMonth, $sYear);
			$sDateLine = InfoLineHTML($$pProduct{'DATE_PROMPT'}, $sDatePrompt, $sDateLineHTML);
			}
		#
		# Display info prompt
		#
		my $sInfoLine;
		if (length $$pProduct{'OTHER_INFO_PROMPT'} > 0)	# if the info is defined, add it to the table
			{
			my $sInfo = $CurrentItem{'INFO'};
			if ($aFailures->[$nLineCount]->{"INFOINPUT"} &&
				defined $aFailures->[$nLineCount]->{"BAD_INFOINPUT"})
				{
				$sInfo = $aFailures->[$nLineCount]->{"BAD_INFOINPUT"};
				}
			$sInfo = InfoHTMLGenerate($$pProduct{'REFERENCE'}, $nLineCount, $sInfo, !$bIncludeButtons, $aFailures->[$nLineCount]->{"INFOINPUT"});
			$sInfo =~ s/%0a/<BR>/g;					# Restore new lines
			$sInfoLine = InfoLineHTML($$pProduct{'OTHER_INFO_PROMPT'}, $sInfo, $sInfoLineHTML);
			}
		#
		# Generate the product line if required
		#
		my $ProdTable;
		my $nRowspan = scalar @aComponentsSeparated + 1;
		if (!$bProductSupressed) 						# the product is not supressed
			{
			$ProdTable = ProductLineHTML($$pProduct{'REFERENCE'}, $sProdLink, $sQuantityText, $sProductLineHTML, $sDuplicateLinkLineHTML, $$pProduct{'DUPLICATES'}, $$pProduct{'THUMBNAIL'}, $bIncludeButtons);
			}
		elsif ($bProductSupressed &&					# or the product is supressed
				 $bIncludeButtons &&						# but the cart is editable
				 !$CurrentItem{'CANEDIT'} == 1)		# and component edit is not allowed
			{
			if (scalar @aComponentsIncluded > 0)
				{
				$nRowspan++;
				}
			$ProdTable = ProductLineHTML($$pProduct{'REFERENCE'}, $sProdLink, $sQuantityText, $sProductLineHTML, $sDuplicateLinkLineHTML, $$pProduct{'DUPLICATES'}, $$pProduct{'THUMBNAIL'}, $bIncludeButtons);
			$sOrderLines .= OrderLineHTML($bIncludeButtons,
													$ProdTable,
													'&nbsp;', 		# no price
													'&nbsp;', 		# no cost
													$sRemove,
													$nRowspan,
													$sOrderLineHTML, $sInfoLine, $sDateLine);
			$ProdTable = "";
			$sRemove = "";
			$nRowspan = -1;
			($sInfoLine, $sDateLine) = ("", "");	# reset prompts if they were used already
			}
		#
		# Check components in this order line
		#
		foreach $pComponent (@aComponentsIncluded)
			{
			$ProdTable .= ProductLineHTML($pComponent->{'REFERENCE'},
													$pComponent->{'NAME'} . $pComponent->{'DDLINK'},
													$pComponent->{'QUANTITY'},
													$sProductLineHTML);
			}

		my $sPrice;
		my $sCost;

		if ($$::g_pSetupBlob{'PRICES_DISPLAYED'})	# if prices are shown
			{
			$sPrice = $CurrentItem{'PRICE'} ? $CurrentItem{'PRICE'} : "--";
			$sCost  = $CurrentItem{'COST'}  ? $CurrentItem{'COST'}  : "--";
			}
		#
		# If we have anything to add.
		#
		if ($ProdTable)
			{
			$sOrderLines .= OrderLineHTML($bIncludeButtons, $ProdTable, $sPrice, $sCost, $sRemove, $nRowspan, $sOrderLineHTML, $sInfoLine, $sDateLine);
			($sInfoLine, $sDateLine) = ("", "");	# reset prompts if they were used already
			}
		#
		# Check components displayed as separate order line
		#
		foreach $pComponent (@aComponentsSeparated)
			{
			my $sCompQty = $pComponent->{'QUANTITY'};
			my $sCompRemove = "";
			my $nCompRowspan = -1;
			if ($CurrentItem{'CANEDIT'} == 1 &&
				 $$pComponent{'CANEDIT'} == 1 &&
				 $bIncludeButtons)
				{
				$sCompQty = $sQuantityText;
				$sCompRemove = $sRemove;
				$nCompRowspan = scalar @aComponentsSeparated;
				}
			my $sProductLines = ProductLineHTML($pComponent->{'REFERENCE'},
													$pComponent->{'NAME'}  . $pComponent->{'DDLINK'},
													$sCompQty,
													$sProductLineHTML);
			$sOrderLines .= OrderLineHTML($bIncludeButtons,
													$sProductLines,
													$pComponent->{'PRICE'},
													$pComponent->{'COST'}, $sCompRemove, $nCompRowspan,
													$sOrderLineHTML, $sInfoLine, $sDateLine);
			}
		#
		# Handle product adjustments
		#
		my $parrProductAdjustments = $pCartObject->GetConsolidatedProductAdjustments($nLineCount);
		my $parrAdjustDetails;
		foreach $parrAdjustDetails (@$parrProductAdjustments)
			{
			@Response = ACTINIC::EncodeText($parrAdjustDetails->[$::eAdjIdxProductDescription]);	# encode description
			my $sProductHTML = ProductLineHTML('', $Response[1], '', $sProductLineHTML);	# get description HTML
			#
			# Format the price and encode the price
			#
			@Response = FormatPrice($parrAdjustDetails->[$::eAdjIdxAmount], $::TRUE, $::g_pCatalogBlob);
			if ($Response[0] != $::SUCCESS)
				{
				return (@Response);
				}
			@Response = ACTINIC::EncodeText($Response[2],$::TRUE,$::TRUE);
			#
			# Add the row to the html
			#
			my $sAdjLine = OrderLineHTML($::FALSE, $sProductHTML, "", $Response[1], '', 0, $sOrderLineHTML);
			if ($bAdjustmentsAtEnd)
				{
				$sProductAdjustmentLines .= $sAdjLine;
				}
			else
				{
				$sOrderLines .= $sAdjLine;
				}
			}
		$nLineCount++;
		}
	#
	# If alternate adjustment display is on then show adjustments at the end of the list
	#
	if ($bAdjustmentsAtEnd)
		{
		$sOrderLines .= $sProductAdjustmentLines;
		}
	$ACTINIC::B2B->SetXML("OrderLine", $sOrderLines);
	#
	# Create variables for substitution
	#
	my %hVariables;
	#
	# if the cart is empty, just print a blank line
	#
	if ($nLineCount == 0)
		{
		$ACTINIC::B2B->SetXML("EmptyCartLine", 1);
		$ACTINIC::B2B->SetXML("OrderLine", "");
		}

	if ($$::g_pSetupBlob{'PRICES_DISPLAYED'} &&		# if prices are displayed
		 $nTotal > 0 && 									# and there is something to show
		 $nLineCount > 0)									# and their are items in the cart
		{														# display the cost panel

		$ACTINIC::B2B->SetXML("SubTotalRow", 1);
		$hVariables{$::VARPREFIX . 'SUBTOTAL'} = ACTINIC::EncodeText2($sSubTotal);
		$ACTINIC::B2B->SetXML("TotalRow", 1);
		$hVariables{$::VARPREFIX . 'TOTAL'} = ACTINIC::EncodeText2($sTotal);
		#
		# Add any order adjustments if present
		#
		my $parrAdjustments = $pCartObject->GetOrderAdjustments();
		my $nAdjustmentCount = scalar(@$parrAdjustments) + scalar($pCartObject->GetFinalAdjustments());
		if($nAdjustmentCount > 0)						# if we have any order adjustments
			{
			my $sAdjustmentsHTML;
			my $parrAdjustDetails;
			foreach $parrAdjustDetails (@$parrAdjustments)	# go through the adjustments
				{
				#
				# Add the HTML for the adjustment to the HTML
				#
				$sAdjustmentsHTML .=
					AdjustmentLineHTML(
						$parrAdjustDetails->[$::eAdjIdxProductDescription],
						$parrAdjustDetails->[$::eAdjIdxAmount],
						$sOrderAdjustmentRowHTML);
				}
			$parrAdjustments = $pCartObject->GetFinalAdjustments();
			foreach $parrAdjustDetails (@$parrAdjustments)	# go through the adjustments
				{
				#
				# Add the HTML for the adjustment to the HTML
				#
				$sAdjustmentsHTML .=
					AdjustmentLineHTML(
						$parrAdjustDetails->[$::eAdjIdxProductDescription],
						$parrAdjustDetails->[$::eAdjIdxAmount],
						$sOrderAdjustmentRowHTML);
				}
			#
			# We don't want to remove the adjustments placeholder
			#
			$ACTINIC::B2B->SetXML("AdjustmentRows", 1);
			#
			# Add the adjustments HTML to the variables hash
			#
			$hVariables{$::VARPREFIX . 'ADJUSTMENTROWS'} = $sAdjustmentsHTML;
			}

		if ($$::g_pSetupBlob{'MAKE_SHIPPING_CHARGE'} && $nShipping != 0)	# if the shipping exists
			{
			@Response = $pCartObject->GetShippingPluginResponse();
			if ($Response[0] != $::SUCCESS)
				{
				return (@Response);
				}
			elsif (${$Response[2]}{GetShippingDescription} != $::SUCCESS)
				{
				return (${$Response[2]}{GetShippingDescription}, ${$Response[3]}{GetShippingDescription});
				}
			my $sShipDescription = $Response[5];

			@Response = ACTINIC::EncodeText($sShipping);	# add the shipping

			my $sCaption = ACTINIC::GetPhrase(-1, 102);

			if ($sShipDescription ne "")				# if there is a shipping description
				{
				$sCaption .= " ($sShipDescription)"; # add the description to the total line
				}

			$ACTINIC::B2B->SetXML("ShippingRow", 1);
			$hVariables{$::VARPREFIX . 'SHIPPINGCAPTION'} = $sCaption;
			$hVariables{$::VARPREFIX . 'SHIPPING'} = $Response[1];
			}
		if ($$::g_pSetupBlob{'MAKE_HANDLING_CHARGE'} && $nHandling != 0)	# if the handling exists
			{
			$ACTINIC::B2B->SetXML("HandlingRow", 1);
			$hVariables{$::VARPREFIX . 'HANDLING'} = ACTINIC::EncodeText2($sHandling);
			}
		if (defined $$::g_pTaxSetupBlob{TAX_1} && $nTax1 > 0)	# if the tax exists
			{													# add the taxt
			my $sTaxName = (defined $$::g_pTaxSetupBlob{'TAX_1'}) ? $$::g_pTaxSetupBlob{'TAX_1'}{'NAME'} : '';	# tax 1 description
			$ACTINIC::B2B->SetXML("Tax1Row", 1);
			$hVariables{$::VARPREFIX . 'TAX1CAPTION'} = ACTINIC::EncodeText2($sTaxName);
			$hVariables{$::VARPREFIX . 'TAX1'} = ACTINIC::EncodeText2($sTax1);
			}
		if (defined $$::g_pTaxSetupBlob{TAX_2} && $nTax2 > 0)	# if the second tax exists
			{													# add the second tax
			my $sTaxName = (defined $$::g_pTaxSetupBlob{'TAX_2'}) ? $$::g_pTaxSetupBlob{'TAX_2'}{'NAME'} : '';	# tax 2 description
			$ACTINIC::B2B->SetXML("Tax2Row", 1);
			$hVariables{$::VARPREFIX . 'TAX2CAPTION'} = ACTINIC::EncodeText2($sTaxName);
			$hVariables{$::VARPREFIX . 'TAX2'} = ACTINIC::EncodeText2($sTax2);
			}
		}
	#
	# Add discount info text if required
	#
	if ($::ReceiptPhase)									# It is not required on the receipt phase
		{														# so empty the discounting info
		$ACTINIC::B2B->SetXML('DiscountInfo', "");
		}
	if ($ACTINIC::B2B->GetXML('DiscountInfo') ne "")	# check if we have anything to be displayed
		{
		my @arrInfoLines = split /\n/, $ACTINIC::B2B->GetXML('DiscountInfo');
		my $sLine;
		my $sFinal;
		foreach $sLine (@arrInfoLines)				# check all info lines now
			{
			if ($sLine)
				{
				$sFinal .= DiscountInfoLineHTML($sLine, $sDiscountInfoLineHTML);
				}
			}
		$ACTINIC::B2B->SetXML("DiscountInfoLine", $sFinal);
		}
	$sOrderLines = ACTINIC::ParseXMLCore(ACTINIC_PXML::GetTemplateFragment($pXML, "ShoppingCart"));
	#
	# Do the substitution for NETQUOTEVARs
	#
	my $sLine;
	($Status, $Message, $sLine) = ACTINIC::TemplateString($sOrderLines, \%hVariables); # make the substitutions
	if ($Status != $::SUCCESS)
		{
		return ($Status, $Message);
		}
	$ACTINIC::B2B->SetXML("ShoppingCart", $sLine);
	return ($::SUCCESS, "", $sLine, 0);
	}

############################################################
#
#  EffectiveCartQuantity - Calculate number of items 'like this' in the cart.
#  This is a dummy function at present simply returning
#  quantity of current cart line. The rest of the function is redundant
#  in this form because CombineCartLines removes identical lines.
#  It should be resurected when 'fuzzy' criteria for items being
#  the same (to qualify for quantity discount) are established.
#
#   Parameters: [0] - reference to current cart item line
#               [1] - reference to the whole cart list
#               [2] - reference to compare function
#               [3] - options to be passed to compare function
#
#   Returns:    [0] - quantity of this product for volume discount purposes
#
#  Compare function is called with three arguments:
#               [0] - reference to first cart line
#               [1] - reference to second cart line
#               [2] - options
#  and returns $::TRUE or $::FALSE if it thinks the two
#  cart lines are sufficiently close to qualify for volume discount together
#  or not respectively
#
#
#  Ryszard Zybert  Dec 12 23:47:22 GMT 2000
#
#  Copyright (c) Actinic Software Ltd (2000)
#
############################################################

sub EffectiveCartQuantity
	{
	my $pCartItem = shift;						# This item in the cart
	my $pCartList = shift;						# The whole cart
	my $pCompare  = shift;						# Compare function
	my $pCompareOpt = shift;					# Compare function options

	if( $ActinicOrder::VDSIMILARLINES == 0 )		# Dont count 'similar' items
		{
		return ($pCartItem->{QUANTITY});
		}

	my $nQuantity = 0;							# quantity of products like this in the cart

	foreach (@$pCartList)						# Loop over cart lines
		{
		if( &$pCompare($_,$pCartItem,$pCompareOpt) == $::TRUE )	# Check if lines are 'the same'
			{
			$nQuantity += $_->{QUANTITY};					# Identical - add quantity
			}
		}
	return ($nQuantity);
	}

############################################################
#
#  IdenticalCartLines - Compare two cart lines.
#   Parameters: [0] - reference to first cart line
#               [1] - reference to second cart line
#               [2] - options (not used at present)
#   Returns:    [0] - $::TRUE if all fields except QUANTITY are the same
#                     $::FALSE if they are not
#
#  Ryszard Zybert  Dec 13 00:02:06 GMT 2000
#
#  Copyright (c) Actinic Software Ltd (2000)
#
############################################################

sub IdenticalCartLines
	{
	my $pCartItem1 = shift;
	my $pCartItem2 = shift;
	my $pOptions	= shift;

	if( $pCartItem1->{QDQUALIFY} eq '1' and $pCartItem2->{QDQUALIFY} eq '1' )
		{
		foreach (keys %$pCartItem1, keys %$pCartItem2)			# Check if all fields are identical...
			{
			if( $_ ne 'QUANTITY' 										# except quantity
				 and $_ !~ /^COMPONENT\_/								# and attributes
				 and $pCartItem1->{$_} ne $pCartItem2->{$_} )	# something is different
				{
				return ($::FALSE);
				}
			}
		}
	else
		{
		foreach (keys %$pCartItem1, keys %$pCartItem2)			# Check if all fields are identical...
			{
			if( $_ ne 'QUANTITY' 										# except quantity
				 and $pCartItem1->{$_} ne $pCartItem2->{$_} )	# something is different
				{
				return ($::FALSE);
				}
			}
		}
		return ($::TRUE);													# Success, they are identical
	}

############################################################
#
#  CalculateCartQuantities - get the Product and Component
#  quantities taking into account the similar items
#
#   Arguments:	0 -
#
#   Returns:   0 - status
#					1 - error message if any
#					2 - the quantities hash
#
#  Zoltan Magyar  Tuesday, July 03, 2001 9:17 PM
#
#  Copyright (c) Actinic Software Ltd (2001)
#
############################################################

sub CalculateCartQuantities
	{
	#
	# If it was called before then pass back the cached value
	#
	if ($::s_bCartQuantityCalculated)
		{
		return($::SUCCESS, "", \%::s_ItemQuantities);
		}
	%::s_ItemQuantities = {};
	#
	# Read the shopping cart
	#
	my @Response;
	@Response = $::Session->GetCartObject($::TRUE);
	if ($Response[0] != $::SUCCESS)					# general error
		{
		return (@Response);								# error so return empty string
		}
	my $pCartObject = $Response[2];

	my $pCartList = $pCartObject->GetCartList();
	my ($pOrderDetail, $pProduct);
	foreach $pOrderDetail (@$pCartList)
		{
		my %CurrentItem = %$pOrderDetail;			# get the next item
		#
		# Locate the section blob
		#
		my ($sSectionBlobName);
		my ($Status, $Message);
		($Status, $Message, $sSectionBlobName) = ACTINIC::GetSectionBlobName($CurrentItem{SID}); # retrieve the blob name
		if ($Status == $::FAILURE)
			{
			return ($Status, $Message);
			}
		#
		# locate this product's object.
		#
		@Response = ACTINIC::GetProduct($CurrentItem{"PRODUCT_REFERENCE"}, $sSectionBlobName,
												  ACTINIC::GetPath());	# get this product object
		($Status, $Message, $pProduct) = @Response;
		if ($Status == $::NOTFOUND)						# the item has been removed from the catalog
			{
			next;
			}
		if ($Status == $::FAILURE)
			{
			return (@Response);
			}
		#
		# Store the quantitiy
		#
		$::s_ItemQuantities{$CurrentItem{"PRODUCT_REFERENCE"}} += $CurrentItem{QUANTITY};
		#
		# Check component quantities
		#
		if( $pProduct->{COMPONENTS} )
			{
			my $VariantList = GetCartVariantList(\%CurrentItem);
			my (%Component, $c);
			my $nIndex = 1;
			foreach $c (@{$pProduct->{COMPONENTS}})
				{
				@Response = FindComponent($c,$VariantList);
				($Status, %Component) = @Response;
				if ($Status != $::SUCCESS)
					{
					return ($Status,$Component{text});
					}
				if( $Component{quantity} > 0 )		# do we have any of this component?
					{											# if so then add it to the hash
					my $nComponentQuantity = $CurrentItem{QUANTITY} * $Component{quantity};
					if ($Component{code} &&				# is it an associated product?
						 ($c->[$::CBIDX_ASSOCPRODPRICE] == 1 ||	# and associated product price is used?
						 $Component{'AssociatedPrice'}))
						{										# it shouldn't be handled separately, so index with product reference
						$::s_ItemQuantities{$Component{code}} += $nComponentQuantity;
						}
					else										# otherwise generate special identifier
						{										# and use that as key
						$::s_ItemQuantities{$CurrentItem{"PRODUCT_REFERENCE"} . "_" . $nIndex} += $nComponentQuantity;
						}
					}
				$nIndex++;
				}
			}
		}
	$::s_bCartQuantityCalculated = $::TRUE;
	return($::SUCCESS, "", \%::s_ItemQuantities)
	}

############################################################
#
#  CalculateSchPrice - get price from Product object
#  Returns price based on quantity and price schedule
#
#   Arguments:	0 - $pProduct - product object reference
#  	        	1 - $Quantity (if undefined, just get a regular price)
#              2 - $sDigest - MD5 digest of user id and password
#              3 - optional schedule price index (if defined, don't match, just get price)
#
#   Returns:   0 - price
#
#  Ryszard Zybert  Dec 20 22:18:09 GMT 1999
#
#  Copyright (c) Actinic Software Ltd (1999)
#
############################################################

sub CalculateSchPrice
	{
	#
	# This function seems to be used only for cart value calculation.
	# The passed $Quantity seems to be the EffectiveCartQuantity
	# all the time. However this value doesn't take into account
	# the similar but not same items. So I have introduced
	# CalculateCartQuantities() to determine the exact value of each
	# product and component in the cart.
	#
	my ($pProduct,$Quantity,$sDigest,$nIndex) = @_;

	my $Price = $pProduct->{PRICE};
	if( defined($Quantity) )							# No quantity, no schedule, no nothing
		{
		#
		# Read the existing items
		#
		my @Response = CalculateCartQuantities();
		if ($Response[0] != $::SUCCESS)				# general error
			{
			return (@Response);							# error so return
			}
		#
		# If the cart already contains any of this item then modify
		# the quantity to get valid prices
		#
		if ($Response[2]{$pProduct->{"REFERENCE"}})
			{
			$Quantity = $Response[2]{$pProduct->{"REFERENCE"}};
			}
		my $SchPrice = GetSchedulePrices($pProduct,$sDigest);	# An array of price information
		if( $SchPrice )									# Anything fits?
			{
			if( defined($nIndex) )
				{
				$Price = $SchPrice->[$nIndex]->[1];
				}
			else
				{
				my $MaxFound = -1;
				foreach (@{$SchPrice})					# Find best fit for this quantity
					{
					if( $_->[0] > $MaxFound and $Quantity >= $_->[0] )
						{
						$MaxFound = $_->[0];
						$Price    = $_->[1];
						}
					}
				}
			}
		}
	return ($Price);
	}

############################################################
#
#  GetComponentPrice - extract component price
#  Arguments
#  	  0 - price element of component structure
#  	  1 - quantity (for parent product)
#       2 - quantity (component per parent product)
#       3 - optional schedule index (overrides user schedule if present)
# 		  4 - component reference (optional, if not defined the cart content is not
#				used for QD price calculation)
#  Returns
#       0 - status
#       1 - message
#  	  2 - component price (per one parent product)
#
#  If price on input is a number it is returned.
#  Otherwise we check if there is a user logged in and find a schedule
#  price for this user (or retail if there is no user) and this
#  quantity.
#
#  Ryszard Zybert  Apr  4 14:49:54 BST 2000
#
#  Zoltan Magyar  11:30 AM July 10, 2001
#		- Cart content dependent price calculation added
#
#  Copyright (c) Actinic Software Ltd (2000)
#
############################################################

sub GetComponentPrice
	{
	my $pPrice = shift;
	my $Quantity = shift;
	my $CompQuantity = shift;
	my $nSchedule = shift;
	my $sReference = shift;
	#
	# The old design was to count 1 component / product
	# caltulating the price. It has been changed to count the components
	#
	# When the product qty is 0 then it is price break checking so
	# we can use 1 to get correct price
	#
	if ($Quantity == 0)
		{
		$Quantity = 1;
		}
	$Quantity *= $CompQuantity;
	#
	# If $sReference is defined then the price is calculated for cart display
	# so lets take the cart content into account
	#
	if (defined $sReference)
		{
		#
		# Read the existing items
		#
		my @Response = CalculateCartQuantities();
		if ($Response[0] != $::SUCCESS)				# general error
			{
			return (@Response);							# error so return
			}
		if ($Response[2]{$sReference})
			{
			$Quantity = $Response[2]{$sReference};
			}
		}

	if( ref($pPrice) ne 'HASH' )					# If it is just a simple price, return it
		{
		if( ref($pPrice) eq 'ARRAY' )
			{
			return ($::SUCCESS, undef, $pPrice->[0] * $CompQuantity);
			}
		return ($::SUCCESS, undef, $pPrice * $CompQuantity);
		}
	my $sDigest = $ACTINIC::B2B->Get('UserDigest');		# Check if there is a user
	my $nIndex = GetScheduleID($sDigest);

	if( defined($nSchedule) )								# If Schedule argument present it takes precedence
		{
		$nIndex = $nSchedule;
		}

	my $SchPrice = $pPrice->{$nIndex};	# An array of price information
	my $Price;
	my $MaxFound = -1;
	foreach (@{$SchPrice})									# Find best fit for this quantity
		{
		if( $_->[0] > $MaxFound and $Quantity >= $_->[0] )
			{
			$MaxFound = $_->[0];
			$Price    = $_->[1];
			}
		}
	if( $MaxFound == -1 )										# Nothing found - return retail price then
		{
		$SchPrice = $pPrice->{'1'};							# Default to retail
		foreach (@{$SchPrice})									# Find best fit for this quantity
			{
			if( $_->[0] > $MaxFound and $Quantity >= $_->[0] )
				{
				$MaxFound = $_->[0];
				$Price    = $_->[1];
				}
			}
		}
	return ($::SUCCESS, undef, $Price * $CompQuantity);
	}

############################################################
#
#  GetScheduleID - get price schedule ID for a customer
#
#  Arguments
#  	  0 - user digest
#
#  Returns
#  	  0 - the schedule ID for the logged in customer or
#				RETAILID for no logins
#
#  ZoltanM  Saturday, October 13, 2001
#
############################################################

sub GetScheduleID
	{
	my $sDigest = shift @_;

	my $nScheduleID = $ActinicOrder::RETAILID;	# default to retail for no logins
	if ($sDigest)											# B2B
		{
		#
		# If we have been called from an third party site or from the java applet
		# via IE on the Mac, there is no cookie information.  When logging in as a registered
		# customer the price schedule is saved to the chk file so we use this instead of
		# getting the account, which may fail
		#
		if(defined $::g_PaymentInfo{'SCHEDULE'} &&
			$::g_PaymentInfo{'SCHEDULE'} >= $ActinicOrder::RETAILID)
			{
			$nScheduleID = $::g_PaymentInfo{'SCHEDULE'}; # get the schedule ID
			}
		else
			{
			my ($Status, $sMessage, $pBuyer, $pAccount);
			($Status, $sMessage, $pBuyer) = ACTINIC::GetBuyer($sDigest, ACTINIC::GetPath());
			if ($Status != $::SUCCESS)
				{
				ACTINIC::ReportError($sMessage, ACTINIC::GetPath());	# Backing in error handling is not practical at this point
				}

			($Status, $sMessage, $pAccount) = ACTINIC::GetCustomerAccount($$pBuyer{AccountID}, ACTINIC::GetPath());
			if ($Status != $::SUCCESS)
				{
				ACTINIC::ReportError($sMessage, ACTINIC::GetPath());	# Backing in error handling is not practical at this point
				}

			$nScheduleID = $pAccount->{PriceSchedule}; # get the schedule ID
			}
		}
	return($nScheduleID);
	}

############################################################
#  GetSchedulePrices - get price schedule for a customer
#  Arguments
#  	  0 - reference to product structure
#  	  1 - user digest
#  Returns
#  	  0 - reference to schedule prices hash or
#           the fixed price
#
#  Ryszard Zybert  Apr  4 14:34:17 BST 2000
#
#  Copyright (c) Actinic Software Ltd (2000)
############################################################

sub GetSchedulePrices
	{
	my ($pProduct,$sDigest) = @_;

	my $nScheduleID = GetScheduleID($sDigest);

	return ($pProduct->{PRICES}->{$nScheduleID});
	}

############################################################
#
#  ArrayAdd - summarize the array item by item 
#		+= operator for arrays
#
#  Arguments
#  	  0 - LHS array
#  	  1 - RHS array
#
#  Zoltan Magyar  Tuesday, February 24, 2004
#
############################################################

sub ArrayAdd
	{
	my ($aLeft, $aRight) = @_;
	my $nIndex;
	for ($nIndex = 0; $nIndex <= ($#$aRight > $#$aLeft ? $#$aRight : $#$aLeft); $nIndex++)
		{
		$$aLeft[$nIndex] += $$aRight[$nIndex];
		}
	}
	
#######################################################
#
# SummarizeOrder - summarize the order and return
#	the values.
#
# Params:	0 - pointer to the cart list
#				1 - optional - flag indicating
#					how to handle advanced shipping
#					errors.  if $::TRUE, ignore them.
#					Default - $::FALSE
#
# Returns:	0 - status
#				1 - error
#				2 - sub total
#				3 - shipping
#				4 - tax 1
#				5 - tax 2
#				6 - total
#				7 - tax 1 on shipping (fraction of 4 that is
#					due to shipping)
#				8 - tax 2 on shipping (fraction of 5 that is
#					due to shipping)
#				9 - handling
#				10 - tax 1 on handling (fraction of 4 that is
#					due to handling)
#				11 - tax 2 on handling (fraction of 5 that is
#					due to handling)
#
#######################################################

sub SummarizeOrder
	{
#? ACTINIC::ASSERT($#_ >= 0, "Parameters not set to SummarizeOrder ($#_)", __LINE__, __FILE__);
	no strict 'refs';
	if ($#_ < 0)
		{
		return ($::FAILURE, ACTINIC::GetPhrase(-1, 12, 'SummarizeOrder'), 0, 0);
		}
	my (@Response, @DefaultTaxResponse, $nStatus, $sMessage, $pCartObject, $pCartList, $bIgnoreAdvancedErrors);
	($pCartList) = $_[0];
	$bIgnoreAdvancedErrors = $::FALSE;
	if ($#_ == 1)
		{
		$bIgnoreAdvancedErrors = $_[1];
		}
	#
	# Update the tax location information
	#
	ParseAdvancedTax();
	#
	# Get the cart object
	#
	($nStatus, $sMessage, $pCartObject) = $::Session->GetCartObject($::TRUE);
	if ($nStatus != $::SUCCESS)						# general error
		{
		return ($nStatus, $sMessage);					# return error
		}
	#
	# Process any product adjustments
	#
	@Response = $pCartObject->ProcessProductAdjustments();
	if ($Response[0] != $::SUCCESS)					# general error
		{
		return (@Response);								# return error
		}
	my ($pOrderDetail, $nSubTotal, $nTax1, $nTax2, %CurrentItem, $pProduct, $sTax1Band, $sTax2Band);
	$nSubTotal = 0;
	my $nCartIndex = 0;
	my (@nShipPrices, @sShipProducts, @nShipQuantities);
	my $sDigest = $ACTINIC::B2B->Get('UserDigest');			# Get User ID once
	#
	# Keep track of unique tax bands in hashes
	#
	my ($nProductSubTotal, $nAdjustedProductSubTotal, $nSubTotalPlusShipHand, $nSubTotalPlusShipHandTax1, $nSubTotalPlusShipHandTax2);
	my @aProductSubTotalTax;
	my $nProductAdjustments;
	my @aProductAdjustmentsTax;
	my @aAdjustedProductSubTotalTax;
	foreach $pOrderDetail (@$pCartList)
		{
		my $nPrice;
		#
		# Get the product price 
		#
		my $nProductPrice;
		my @aProductTax;

		($nStatus, $sMessage, 
			$nProductPrice, 	$nPrice,
			$sTax1Band,			$sTax2Band,
			@aProductTax) = $pCartObject->GetCartItemPrice($pOrderDetail);
		if ($nStatus == $::FAILURE)				# general error
			{
			return ($nStatus, $sMessage);			# return error
			}		
		if ($nStatus == $::NOTFOUND)				# product not found?
			{
			next;											# take next then
			}		
		$nProductSubTotal				+= $nProductPrice;
		ArrayAdd(\@aProductSubTotalTax, \@aProductTax);
		($nStatus, $sMessage, $pProduct) = Cart::GetProduct($$pOrderDetail{"PRODUCT_REFERENCE"}, $$pOrderDetail{SID});	# get this product object
		if ($nStatus != $::SUCCESS)
			{
			return ($nStatus, $sMessage);
			}		
		#
		# Handle any product adjustments
		#
		my $parrProductAdjustments =
			$pCartObject->GetProductAdjustments($nCartIndex);	# get the product adjustments
		@Response = CalculateProductAdjustments($parrProductAdjustments, $pProduct, $sTax1Band, $sTax2Band, $$pProduct{"PRICE"});
		if ($Response[0] != $::SUCCESS)
			{
			return (@Response);
			}
		my ($nAdjustments, @aAdjustmentsTax) = @Response[2..10];
		$nProductAdjustments += $nAdjustments;
		ArrayAdd(\@aProductAdjustmentsTax, \@aAdjustmentsTax);
		#
		# store the product information for use with the shipping plug in
		#
		push (@sShipProducts, $CurrentItem{"PRODUCT_REFERENCE"});
		push (@nShipQuantities, $CurrentItem{"QUANTITY"});
		push (@nShipPrices, $nPrice);

		$nCartIndex++;
		}
	#
	# Set the totals after product adjustments have been applied
	#
	$nAdjustedProductSubTotal = $nProductSubTotal + $nProductAdjustments;
	ArrayAdd(\@aAdjustedProductSubTotalTax, \@aProductSubTotalTax);
	ArrayAdd(\@aAdjustedProductSubTotalTax, \@aProductAdjustmentsTax);		
	my ($nProductTax1, $nProductTax2) = ($nTax1, $nTax2);
	#
	# If tax info is not known then indicate it for the discount plugin by
	# passing in undef
	#
	my @arrTotalsAndTaxes =
			(
			[$nProductSubTotal, @aProductSubTotalTax[0..3], TaxIsKnown(), @aProductSubTotalTax[4..7]],
			[$nAdjustedProductSubTotal, @aAdjustedProductSubTotalTax[0..3], TaxIsKnown(), @aAdjustedProductSubTotalTax[4..7]],
			);
	#
	# Handle any pre-Shipping and Handling order adjustments
	#
	my $parrAdjustments;
	($nStatus, $sMessage, $parrAdjustments) = $pCartObject->ProcessOrderAdjustments(\@arrTotalsAndTaxes);
	if ($nStatus != $::SUCCESS)						# general error
		{
		return ($nStatus, $sMessage);					# return error
		}
	@Response = CalculateOrderAdjustments($parrAdjustments,
		$nProductSubTotal, $aProductSubTotalTax[4], $aProductSubTotalTax[5],
		\@arrTotalsAndTaxes);

	if ($Response[0] != $::SUCCESS)
		{
		return (@Response);
		}
	my ($nAdjustments, $nAdjustmentsTax1, $nAdjustmentsTax2, 
							 $nUAdjustmentsTax1, $nUAdjustmentsTax2) = @Response[2..6];
	my $nOrderAdjustedSubTotal = $nAdjustedProductSubTotal + $nAdjustments;
	my $nOrderAdjustedSubTotalTax1 = $aAdjustedProductSubTotalTax[0] + $nAdjustmentsTax1;
	my $nOrderAdjustedSubTotalTax2 = $aAdjustedProductSubTotalTax[1] + $nAdjustmentsTax2;
	my $nUOrderAdjustedSubTotalTax1 = $aAdjustedProductSubTotalTax[4] + $nUAdjustmentsTax1;
	my $nUOrderAdjustedSubTotalTax2 = $aAdjustedProductSubTotalTax[5] + $nUAdjustmentsTax2;	
	#
	# Add the new total and taxes to our array
	#
	push @arrTotalsAndTaxes, [$nOrderAdjustedSubTotal, $nOrderAdjustedSubTotalTax1, $nOrderAdjustedSubTotalTax2, 0, 0, 0, , $nUOrderAdjustedSubTotalTax1, $nUOrderAdjustedSubTotalTax2];
	#
	# calculate the shipping
	#
	my ($nShipping);
	if ($$::g_pSetupBlob{'MAKE_SHIPPING_CHARGE'})	# shipping is enabled
		{
		my @Response = CallShippingPlugIn($pCartList, $nOrderAdjustedSubTotal);
		#
		# Save the reponse to the cart object
		#
		$pCartObject->SetShippingPluginResponse(\@Response);
		if ($Response[0] != $::SUCCESS ||			# execute the script
			 ${$Response[2]}{CalculateShipping} != $::SUCCESS)
			{
			if ($bIgnoreAdvancedErrors)
				{
				$Response[6] = 0;
				}
			else
				{
				if ($Response[0] != $::SUCCESS)
					{
					return (@Response);
					}
				elsif (${$Response[2]}{CalculateShipping} != $::SUCCESS)
					{
					return (${$Response[2]}{CalculateShipping}, ${$Response[3]}{CalculateShipping});
					}
				}
			}
		$nShipping = $Response[6];
		}
	else														# no shipping
		{
		$nShipping = 0;
		}
	#
	# Get the shipping tax opaque data
	#
	@Response = GetShippingTaxBands();
	if ($Response[0] != $::SUCCESS)
		{
		return (@Response);
		}
	#
	# calculate the tax on the shipping
	#
   @Response = CalculateTax($nShipping, 1, $Response[2], $Response[3], $nShipping,
		$nOrderAdjustedSubTotal, $nUOrderAdjustedSubTotalTax1, $nUOrderAdjustedSubTotalTax2);	# calculate taxes on shipping
	if ($Response[0] != $::SUCCESS)
		{
		return (@Response);
		}
	my ($nShippingTax1, $nShippingTax2) = @Response[2,3];
	my ($nUShippingTax1, $nUShippingTax2) = @Response[4,5];
	#
	# add the shipping tax to the product tax totals
	#
	$nTax1 = $nOrderAdjustedSubTotalTax1 + $nShippingTax1;	# calculate the tax 1 composite total
	$nTax2 = $nOrderAdjustedSubTotalTax2 + $nShippingTax2;	# calculate the tax 2 composite total	
	my $nUTax1 = $nUOrderAdjustedSubTotalTax1 + $nUShippingTax1;	# calculate the unrounded tax 1 composite total
	my $nUTax2 = $nUOrderAdjustedSubTotalTax2 + $nUShippingTax2;	# calculate the unrounded tax 2 composite total		
	#
	# calculate the handling
	#
	my ($nHandling);
	if ($$::g_pSetupBlob{'MAKE_HANDLING_CHARGE'})	# shipping is enabled
		{
		my @Response = CallShippingPlugIn($pCartList, $nOrderAdjustedSubTotal);
		#
		# Save the reponse to the cart object
		#
		$pCartObject->SetShippingPluginResponse(\@Response);
		if ($Response[0] != $::SUCCESS ||			# execute the script
			 ${$Response[2]}{CalculateHandling} != $::SUCCESS)
			{
			if ($bIgnoreAdvancedErrors)
				{
				$Response[8] = 0;
				}
			else
				{
				if ($Response[0] != $::SUCCESS)
					{
					return (@Response);
					}
				elsif (${$Response[2]}{CalculateHandling} != $::SUCCESS)
					{
					return (${$Response[2]}{CalculateHandling}, ${$Response[3]}{CalculateHandling});
					}
				}
			}
		$nHandling = $Response[8];
		}
	else														# no handling
		{
		$nHandling = 0;
		}
	#
	# Get the handling tax opaque data
	#
	@Response = GetHandlingTaxBands();
	if ($Response[0] != $::SUCCESS)
		{
		return (@Response);
		}
	#
	# calculate the tax on the handling
	#
	@Response = CalculateTax($nHandling, 1, $Response[2], $Response[3], $nHandling);	# calculate taxes on this product
	if ($Response[0] != $::SUCCESS)
		{
		return (@Response);
		}
	my ($nHandlingTax1, $nHandlingTax2) = @Response[2,3];
	my ($nUHandlingTax1, $nUHandlingTax2) = @Response[4,5];
	#
	# add the handling tax to the product tax totals
	#
	$nTax1 += $nHandlingTax1;							# calculate the tax 1 composite total
	$nTax2 += $nHandlingTax2;							# calculate the tax 2 composite total
	$nUTax1 += $nUHandlingTax1;						# calculate the unrounded tax 1 composite total
	$nUTax2 += $nUHandlingTax2;						# calculate the unrounded tax 2 composite total
	
	my $nOrderTotalPlusShipHand = $nOrderAdjustedSubTotal + $nShipping + $nHandling;
	my $nOrderTotalPlusShipHandTax1 = $nTax1;
	my $nOrderTotalPlusShipHandTax2 = $nTax2;
	#
	# Add the new total and taxes to our array
	#
	push @arrTotalsAndTaxes, [$nOrderTotalPlusShipHand, 
				$nOrderTotalPlusShipHandTax1, $nOrderTotalPlusShipHandTax2, 0, 0, 0,
				$nUOrderAdjustedSubTotalTax1 + $nUHandlingTax1 + $nUShippingTax1,
				$nUOrderAdjustedSubTotalTax2 + $nUHandlingTax2 + $nUShippingTax2];
	#
	# Handle any final order adjustments after shiping and handling
	#
	($nStatus, $sMessage, $parrAdjustments) = $pCartObject->ProcessFinalAdjustments(\@arrTotalsAndTaxes);
	if ($nStatus != $::SUCCESS)						# general error
		{
		return ($nStatus, $sMessage);					# return error
		}
	@Response = CalculateOrderAdjustments($parrAdjustments, $nOrderTotalPlusShipHand, 
		$nUOrderAdjustedSubTotalTax1 + $nUHandlingTax1 + $nUShippingTax1,
		$nUOrderAdjustedSubTotalTax2 + $nUHandlingTax2 + $nUShippingTax2,
		\@arrTotalsAndTaxes);

	if ($Response[0] != $::SUCCESS)
		{
		return (@Response);
		}
	#my ($nUAdjustmentsTax1, $nUAdjustmentsTax2);
	($nAdjustments, $nAdjustmentsTax1, $nAdjustmentsTax2, $nUAdjustmentsTax1, $nUAdjustmentsTax2) = @Response[2..4];
	$nOrderTotalPlusShipHand += $nAdjustments;
	$nTax1 += $nAdjustmentsTax1;
	$nTax2 += $nAdjustmentsTax2;
	$nUTax1 += $nUAdjustmentsTax1;
	$nUTax2 += $nUAdjustmentsTax2;	
	#
	# We've finished with the taxes so we round the total tax
	# if the rounding is per order
	#
	if (defined $$::g_pTaxSetupBlob{'TAX_1'} &&
		$$::g_pTaxSetupBlob{'TAX_1'}{'ROUND_GROUP'} == $ActinicOrder::ROUNDPERORDER)
		{
		$nTax1 = RoundTax($nUTax1,
			$$::g_pTaxSetupBlob{'TAX_1'}{'ROUND_RULE'});
		#
		# we've used the unrounded taxes for getting the total taxes so now round to the nearest
		# currency unit
		#
		$nShippingTax1 = RoundTax($nUShippingTax1,
			$ActinicOrder::SCIENTIFIC_NORMAL);
		$nHandlingTax1 = RoundTax($nUHandlingTax1,
			$ActinicOrder::SCIENTIFIC_NORMAL);
		}
	if (defined $$::g_pTaxSetupBlob{'TAX_2'} &&
		$$::g_pTaxSetupBlob{'TAX_2'}{'ROUND_GROUP'} == $ActinicOrder::ROUNDPERORDER)
		{
		$nTax2 = RoundTax($nUTax2,
			$$::g_pTaxSetupBlob{'TAX_2'}{'ROUND_RULE'});
		#
		# we've used the unrounded taxes for getting the total taxes so now round to the nearest
		# currency unit
		#
		$nShippingTax2 = RoundTax($nUShippingTax2,
			$ActinicOrder::SCIENTIFIC_NORMAL);
		$nHandlingTax2 = RoundTax($nUHandlingTax2,
			$ActinicOrder::SCIENTIFIC_NORMAL);
		}

	#
	# check the tax exemption
	#
	if ($::g_TaxInfo{'EXEMPT1'})						# if they've declared exemption
		{
		$nTax1 = 0;											# exempt the tax
		$nShippingTax1 = 0;
		$nHandlingTax1 = 0;
		}
	if ($::g_TaxInfo{'EXEMPT2'})
		{
		$nTax2 = 0;
		$nShippingTax2 = 0;
		$nHandlingTax2 = 0;
		}
	#
	# the numbers are all complete at this point
	#
	my $nTotal = $nOrderTotalPlusShipHand + $nTax1 + $nTax2;

	return ($::SUCCESS, "", $nAdjustedProductSubTotal, $nShipping, $nTax1, $nTax2, $nTotal, $nShippingTax1, $nShippingTax2,
		$nHandling, $nHandlingTax1, $nHandlingTax2);
	}

#######################################################
#
# GetCartVariantList - get a list of variants/components
#			associated with a cart item.
#
# Input:	$phashCart	- ref to cart hash
#
# Returns:	0 - ref to list of variants
#
# Author:	Mike Purnell
#
#######################################################

sub GetCartVariantList
	{
	my ($phashCart) = @_;
	my ($plistVariants, $sKey);
	$plistVariants = [];
	foreach $sKey (keys %$phashCart)
		{
		if( $sKey =~ /^COMPONENT\_/ )
			{
			$plistVariants->[$'] = $phashCart->{$sKey};
			}
		}
	return($plistVariants);
	}

#######################################################
#
# CalculateProductAdjustments - sum the product adjustments
#	and calculate the tax.
#
# Input:	$parrAdjustments			- ref to array of adjustments
#			$pProduct					- ref to the main product
#			$sProductTax1Band			- the tax 1 opaque data
#			$sProductTax2Band			- the tax 2 opaque data
#			$nProductRetailPrice		- the retail price of the product
#
# Returns:	0 - status
#				1 - error
#				2 - total adjustments on this product
#				3 - total tax 1 on these adjustments
#				4 - total tax 2 on these adjustments
#
# Author:	Mike Purnell
#
#######################################################

sub CalculateProductAdjustments
	{
	my ($parrAdjustments, $pProduct, $sProductTax1Band, $sProductTax2Band, $nProductRetailPrice) = @_;
	my ($nAdjustments, $sTax1Band, $sTax2Band, $nRetailPrice);
	my ($nAdjustmentsTax1, $nAdjustmentsTax2, $nAdjustmentsDefTax1, $nAdjustmentsDefTax2);
	my ($nUAdjustmentsTax1, $nUAdjustmentsTax2, $nUAdjustmentsDefTax1, $nUAdjustmentsDefTax2);
	
	my ($parrAdjustDetails, @arrResponse);

	foreach $parrAdjustDetails (@$parrAdjustments)	# go through the adjustments
		{
		$nAdjustments += $parrAdjustDetails->[$::eAdjIdxAmount];
		#
		# Calculate the tax on the adjusted product sub-total
		#
		my $sProdRef = $parrAdjustDetails->[$::eAdjIdxTaxProductRef];
		if($sProdRef ne '' &&
			$pProduct->{'REFERENCE'} ne $sProdRef)
			{
			#
			# Get the associated product
			#
			my($nStatus, $sMessage, $pAssociatedProduct) =
				GetComponentAssociatedProduct($pProduct, $sProdRef);
			if($nStatus != $::SUCCESS)
				{
				return($nStatus, $sMessage);
				}
			#
			# Get the tax bands for the associated product
			#
			($nStatus, $sMessage, $sTax1Band, $sTax2Band) =
				GetProductTaxBands($pAssociatedProduct);
			if ($nStatus != $::SUCCESS)
				{
				return ($nStatus, $sMessage);
				}
			$nRetailPrice = $pAssociatedProduct->{'PRICE'};
			}
		else
			{
			$sTax1Band = $sProductTax1Band;
			$sTax2Band = $sProductTax2Band;
			$nRetailPrice = $nProductRetailPrice;
			}
		if($parrAdjustDetails->[$::eAdjIdxCustomTaxAsExempt])
			{
			my ($nBandID, $nPercent, $nFlatRate, $sBandName) = split /=/, $sTax1Band;
			if ($nBandID == $ActinicOrder::CUSTOM)
				{
				$sTax1Band = '1=0=0=';
				}
			($nBandID, $nPercent, $nFlatRate, $sBandName) = split /=/, $sTax2Band;
			if ($nBandID == $ActinicOrder::CUSTOM)
				{
				$sTax2Band = '1=0=0=';
				}
			}
		@arrResponse = CalculateTax($parrAdjustDetails->[$::eAdjIdxAmount], 1, $sTax1Band, $sTax2Band, $nRetailPrice);	# calculate taxes on this product
		my @arrDefResponse = CalculateDefaultTax($parrAdjustDetails->[$::eAdjIdxAmount], 1, $sTax1Band, $sTax2Band, $nRetailPrice);	# calculate taxes on this product
		if ($arrResponse[0] != $::SUCCESS)
			{
			return (@arrResponse);
			}
		my ($nAdjustmentTax1, 		$nAdjustmentTax2) 	= @arrResponse[2,3];
		my ($nAdjustmentDefTax1, 	$nAdjustmentDefTax2) = @arrDefResponse[2,3];
		my ($nUAdjustmentTax1, 		$nUAdjustmentTax2) 	= @arrResponse[4,5];
		my ($nUAdjustmentDefTax1, 	$nUAdjustmentDefTax2) = @arrDefResponse[4,5];		
		#
		# Convert product to order detail opaque data
		#
		$sTax1Band = ProductToOrderDetailTaxOpaqueData('TAX_1',
			$parrAdjustDetails->[$::eAdjIdxAmount],
			$sTax1Band,
			$nRetailPrice);								# convert the tax 1 product opaque data

		$sTax2Band = ProductToOrderDetailTaxOpaqueData('TAX_2',
			$parrAdjustDetails->[$::eAdjIdxAmount],
			$sTax2Band,
			$nRetailPrice);								# convert the tax 2 product opaque data
		#
		# Save the tax details to the adjustment array
		#
		SetAdjustmentTaxDetails($parrAdjustDetails, $nAdjustmentTax1, $nAdjustmentTax2, $sTax1Band, $sTax2Band);
		#
		# Add to the product adjustment taxes
		#
		$nAdjustmentsTax1 += $nAdjustmentTax1;
		$nAdjustmentsTax2 += $nAdjustmentTax2;
		$nAdjustmentsDefTax1 += $nAdjustmentDefTax1;
		$nAdjustmentsDefTax2 += $nAdjustmentDefTax2;
		$nUAdjustmentsTax1 += $nUAdjustmentTax1;
		$nUAdjustmentsTax2 += $nUAdjustmentTax2;
		$nUAdjustmentsDefTax1 += $nUAdjustmentDefTax1;
		$nUAdjustmentsDefTax2 += $nUAdjustmentDefTax2;		
		}
	return ($::SUCCESS, '', $nAdjustments, 
		$nAdjustmentsTax1, $nAdjustmentsTax2, $nAdjustmentsDefTax1, $nAdjustmentsDefTax2,
		$nUAdjustmentsTax1, $nUAdjustmentsTax2, $nUAdjustmentsDefTax1, $nUAdjustmentsDefTax2);
	}

#######################################################
#
# GetComponentAssociatedProduct - get an associated product
#			from it's product ref
#
# Input:	$pProduct		- ref to product
#			$sProdRef		- product reference  of associated product
#
# Returns:	0 - status
#				1 - error
#				2 - ref to associated product
#
# Author:	Mike Purnell
#
#######################################################

sub GetComponentAssociatedProduct
	{
	my($pProduct, $sProdRef) = @_;
	my($nStatus, $sMessage, $pComponent);

	foreach $pComponent (@{$pProduct->{'COMPONENTS'}})							# go through the product hash
		{
		my $pPermutation;
		foreach $pPermutation (@{$pComponent->[$::CBIDX_PERMUTATIONS]})	# go through permutation list
			{
			my $pAssocProd = $pPermutation->[$::PBIDX_ASSOCIATEDPROD];		# get putative associated product
			if(ref($pAssocProd) eq 'HASH' &&											# if we have a product hash
				$pAssocProd->{'REFERENCE'} eq $sProdRef)							# and it matches our ref
				{
				return($::SUCCESS, '', $pAssocProd);								# return the product hash
				}
			}
		}
	return($::FALSE, "Unable to find associated product, '$sProdRef'");
	}

#######################################################
#
# CalculateOrderAdjustments - sum the order adjustments
#	and calculate the tax.
#
# Input:	$parrAdjustments		- ref to array of adjustments
#			$nTaxBase				- the unadjusted taxable base
#			$nTax1					- the unadjusted tax1 for $nTaxBase
#			$nTax2					- the unadjusted tax2 for $nTaxBase
#			$parrTotalsAndTaxes	- ref to array of product sub totals and taxes
#
# Returns:	0 - status
#				1 - error
#				2 - total adjustments on this product
#				3 - total tax 1 on these adjustments
#				4 - total tax 2 on these adjustments
#
# Author:	Mike Purnell
#
#######################################################

sub CalculateOrderAdjustments
	{
	my ($parrAdjustments, $nTaxBase, $nTax1, $nTax2, $parrTotalsAndTaxes) = @_;
	my ($nAdjustments, $nAdjustmentsTax1, $nAdjustmentsTax2);
	my ($parrAdjustDetails, @arrResponse);
	my $nCartTotal = $nTaxBase;
	my ($nUAdjustmentsTax1, $nUAdjustmentsTax2);
	my ($nUAdjustmentsTax1Sum, $nUAdjustmentsTax2Sum);
	
	foreach $parrAdjustDetails (@$parrAdjustments)	# go through the adjustments
		{
		$nTaxBase += $parrAdjustDetails->[$::eAdjIdxAmount];
		$nAdjustments += $parrAdjustDetails->[$::eAdjIdxAmount];
		#
		# Calculate the tax on the adjusted product sub-total
		#
		my ($sTax1Band, $sTax2Band);
		my ($nStatus, $sMessage, $pAssociatedProduct);
		$pAssociatedProduct = undef;
		#
		# If we're not taxing as a product, set tax bands to pro rata
		#
		# NOTE: the adjustment taxation as product is not implemented in this version
		# Extend the lines below if this gets implemented once
		#
		# if($parrAdjustDetails->[$::eAdjIdxTaxTreatment] != $::eAdjTaxAsProduct)
		#
		if(defined $$::g_pTaxSetupBlob{TAX_1})	# tax 1 levied?
			{
			$sTax1Band = '5=0=0=';
			}
		if(defined $$::g_pTaxSetupBlob{TAX_2})	# tax 2 levied?
			{
			$sTax2Band = '5=0=0=';
			}

		my $nTotalsIndex = 0;
		my $parrTotalAndTaxes = $parrTotalsAndTaxes->[0];
		if($parrAdjustDetails->[$::eAdjIdxTaxTreatment] == $::eAdjTaxProRata)
			{
			$parrTotalAndTaxes = $parrTotalsAndTaxes->[0];
			}
		elsif($parrAdjustDetails->[$::eAdjIdxTaxTreatment] == $::eAdjTaxProRataAdjusted)
			{
			$parrTotalAndTaxes = $parrTotalsAndTaxes->[1];
			}
		elsif($parrAdjustDetails->[$::eAdjIdxTaxTreatment] == $::eAdjTaxProRataTotal)
			{
			$parrTotalAndTaxes = $parrTotalsAndTaxes->[3];
			}
		#
		# Check if default or actual tax is used
		#
		my ($nUAdjustmentsTax1, $nUAdjustmentsTax2);
		if($parrAdjustDetails->[$::eAdjIdxAdjustmentBasis] == 1)
			{
			$nUAdjustmentsTax1 = $parrTotalAndTaxes->[8] * $parrAdjustDetails->[$::eAdjIdxAmount] / $parrTotalAndTaxes->[0];
			$nUAdjustmentsTax2 = $parrTotalAndTaxes->[9] * $parrAdjustDetails->[$::eAdjIdxAmount] / $parrTotalAndTaxes->[0];
			}		
		else
			{
			$nUAdjustmentsTax1 = $parrTotalAndTaxes->[6] * $parrAdjustDetails->[$::eAdjIdxAmount] / $parrTotalAndTaxes->[0];
			$nUAdjustmentsTax2 = $parrTotalAndTaxes->[7] * $parrAdjustDetails->[$::eAdjIdxAmount] / $parrTotalAndTaxes->[0];
			}
		#
		# Calculate the tax of the adjustment by taking the difference from
		# the previously adjusted tax
		#
		$nUAdjustmentsTax1Sum += $nUAdjustmentsTax1;
		$nUAdjustmentsTax2Sum += $nUAdjustmentsTax2;
		my $nTax1Diff = RoundScientific($nUAdjustmentsTax1);
		my $nTax2Diff = RoundScientific($nUAdjustmentsTax2);
		#
		# Add to the product adjustment taxes
		#
		$nAdjustmentsTax1 += $nTax1Diff;
		$nAdjustmentsTax2 += $nTax2Diff;
		#
		# Convert product to order detail opaque data
		#
		$sTax1Band = ProductToOrderDetailTaxOpaqueData('TAX_1',
			$parrAdjustDetails->[$::eAdjIdxAmount],
			$sTax1Band,
			$parrAdjustDetails->[$::eAdjIdxAmount]);	# convert the tax 1 product opaque data

		$sTax2Band = ProductToOrderDetailTaxOpaqueData('TAX_2',
			$parrAdjustDetails->[$::eAdjIdxAmount],
			$sTax2Band,
			$parrAdjustDetails->[$::eAdjIdxAmount]);	# convert the tax 2 product opaque data

		#
		# Save the tax details to the adjustment array
		#
		if(defined $pAssociatedProduct)
			{
			SetAdjustmentTaxDetails($parrAdjustDetails, $nTax1Diff, $nTax2Diff, $sTax1Band, $sTax2Band, $pAssociatedProduct);
			}
		else
			{
			SetAdjustmentTaxDetails($parrAdjustDetails, $nTax1Diff, $nTax2Diff, $sTax1Band, $sTax2Band);
			}
		}
	return ($::SUCCESS, '', $nAdjustments, $nAdjustmentsTax1, $nAdjustmentsTax2, $nUAdjustmentsTax1Sum, $nUAdjustmentsTax2Sum);
	}

#######################################################
#
# SetAdjustmentTaxDetails - set the tax details in the
#			adjustment array
#
# Input:	$parrAdjustDetails	- ref to adjustment array
#			$nTax1				- tax 1 amount
#			$nTax2				- tax 2 amount
#			$sTax1Band			- the tax 1 opaque data
#			$sTax2Band			- the tax 2 opaque data
#
# Author:	Mike Purnell
#
#######################################################

sub SetAdjustmentTaxDetails
	{
	my ($parrAdjustDetails, $nTax1, $nTax2, $sTax1Band, $sTax2Band) = @_;

	$parrAdjustDetails->[$::eAdjIdxTax1]				= $nTax1;		# tax 1
	$parrAdjustDetails->[$::eAdjIdxTax2]				= $nTax2;		# tax 2
	$parrAdjustDetails->[$::eAdjIdxTax1OpaqueData]	= $sTax1Band;	# tax 1 opaque data
	$parrAdjustDetails->[$::eAdjIdxTax2OpaqueData]	= $sTax2Band;	# tax 2 opaque data
	}

#######################################################
#
# SummarizeOrderPrintable - summarize the order and return
#	the values as formatted text.
#
# Params:	0 - status			The params are formatted
#				1 - error			this way to allow the
#				2 - sub total		response from SummarizeOrder
#				3 - shipping		to be sent directly into
#				4 - tax 1			this function.
#				5 - tax 2
#				6 - total
#				7 - shipping tax 1 (ignored)
#				8 - shipping tax 2 (ignored)
#				9 - handling
#				10 - handling tax 1 (ignored)
#				11 - handling tax 2 (ignored)
#
# Returns:	0 - status
#				1 - error
#				2 - sub total
#				3 - shipping
#				4 - handling
#				5 - tax 1
#				6 - tax 2
#				7 - total
#
#######################################################

sub SummarizeOrderPrintable
	{
#? ACTINIC::ASSERT($#_ == 11, "Parameters not set to SummarizeOrderPrintable ($#_)", __LINE__, __FILE__);

	my ($Status, $Error, $nSubTotal, $nShipping, $nTax1, $nTax2, $nTotal, $nIgnore1, $nIgnore2, $nHandling, $nIgnore3, $nIgnore4) = @_;
	my (@Response);
	if ($Status != $::SUCCESS)
		{
		return (@_);
		}

	#
	# format the prices
	#
	my ($sSubTotal, $sShipping, $sHandling, $sTax1, $sTax2, $sTotal);
	@Response = FormatPrice($nSubTotal, $::TRUE, $::g_pCatalogBlob);
	if ($Response[0] != $::SUCCESS)
		{
		return (@Response);
		}
	$sSubTotal = $Response[2];

	@Response = FormatPrice($nShipping, $::TRUE, $::g_pCatalogBlob);
	if ($Response[0] != $::SUCCESS)
		{
		return (@Response);
		}
	$sShipping = $Response[2];

	@Response = FormatPrice($nHandling, $::TRUE, $::g_pCatalogBlob);
	if ($Response[0] != $::SUCCESS)
		{
		return (@Response);
		}
	$sHandling = $Response[2];

	@Response = FormatPrice($nTax1, $::TRUE, $::g_pCatalogBlob);
	if ($Response[0] != $::SUCCESS)
		{
		return (@Response);
		}
	$sTax1 = $Response[2];

	@Response = FormatPrice($nTax2, $::TRUE, $::g_pCatalogBlob);
	if ($Response[0] != $::SUCCESS)
		{
		return (@Response);
		}
	$sTax2 = $Response[2];

	@Response = FormatPrice($nTotal, $::TRUE, $::g_pCatalogBlob);
	if ($Response[0] != $::SUCCESS)
		{
		return (@Response);
		}
	$sTotal = $Response[2];

	if (! $$::g_pSetupBlob{'MAKE_SHIPPING_CHARGE'})	# if the shipping charge is not made, skip it
		{
		$sShipping = "";									# blank the value
		}

	if (! $$::g_pSetupBlob{'MAKE_HANDLING_CHARGE'})	# if the handling charge is not made, skip it
		{
		$sHandling = "";									# blank the value
		}

	if (!defined $$::g_pTaxSetupBlob{'TAX_1'})				# if tax 1 is not enabled
		{
		$sTax1 = "";										# blank the value
		}

	if (!defined $$::g_pTaxSetupBlob{'TAX_2'})				# if tax 2 is not enabled
		{
		$sTax2 = "";										# blank the value
		}

	return ($::SUCCESS, "", $sSubTotal, $sShipping, $sHandling, $sTax1, $sTax2, $sTotal);
	}

##############################################################################################################
#
# Summarize Order - End
#
##############################################################################################################


##############################################################################################################
#
# Cart Processing - Begin
#
##############################################################################################################

############################################################
#
#  GetComponents - return all components or attributes of a product
#
#	Params: [0] - reference to a product
#   Returns: [0] - hash of components and attributes of the following format:
#
#	{
#		COMPONENTS =>	{
#								<name of component_1> => <component_1>,
#								<name of component_2> => <component_2>,
#								...
#							}
#		ATTRIBUTES =>	{
#								<name of attribute_1> => <attribute_1>,
#								<name of attribute_2> => <attribute_2>,
#								...
#							}
#	}
#
#	where
#
#	<component_i> = {
#							NAME 				=> <name of the component>,
#							INDEX 			=> <index of component>,
#							IS_OPTIONAL 	=> $::TRUE || $::FALSE
#							IS_COMPONENT 	=> $::TRUE
#							ATTRIBUTES 		=> {
#														<name of attribute_1> => <attribute_1>,
#														<name of attribute_2> => <attribute_2>,
#														...
#													}
#							}
#
#	<attribute_i> = 	{
#								NAME 				=> <name of the component>,
#								INDEX 			=> <index of component>,
#								IS_COMPONENT 	=> $::FALSE
#								CHOICES 			=> [<choice_1>, <choice_2>, ...]
#							}
############################################################

sub GetComponents
	{
#? ACTINIC::ASSERT($#_ == 0, "ListComponent has an invalid argument count ($#_).", __LINE__, __FILE__);
	my $pProduct = shift;
	#
	# Initialization
	#
	my $pComponentHash = {COMPONENTS => {}, ATTRIBUTES => {}}; # hash of components
	#
	# Process all components defined in the product definition BLOB
	# and add them to the list
	#
	my $pComponentDefinition;
	foreach $pComponentDefinition (@{$pProduct->{COMPONENTS}})
		{
		my $pComponent = {};								# component object to be composed

		$pComponent->{NAME} 			= $pComponentDefinition->[$::CBIDX_NAME];
		$pComponent->{ATTRIBUTES} 	= {};
		#
		# Is it a
		#		pseudo-component 	-> attributes belong to the product
		#		real one 			-> attributes belong to the component,
		#									component belongs to the product
		#
		my $bRealComponent = !($pComponent->{NAME} eq '');
		#
		# Process attributes
		#
		my $pAttributeDefinition;
		foreach $pAttributeDefinition (@{$pComponentDefinition->[$::CBIDX_ATTRIBUTELIST]})		# Check all attributes
			{
			my $sAttributeName 	= $pAttributeDefinition->[$::ABIDX_NAME];
			my $nAttributeIndex 	= $pAttributeDefinition->[$::ABIDX_WIDGETIDX];
			if ($sAttributeName eq '')					# each component has an 'on' attribute
																# whose name is empty
																# if this pseudo attribute is specified,
																# then the component is a real component
				{
				$pComponent->{IS_OPTIONAL} 	= $pComponentDefinition->[$::CBIDX_OPTIONAL];
				$pComponent->{IS_COMPONENT} 	= $::TRUE;
				$pComponent->{INDEX} 			= $nAttributeIndex; # a component's index is the index of its 'on' attribute
				}
			else												# this is a real attribute of the components
				{
				my $pAttribute = {};						# attribute object to be composed
				$pAttribute->{INDEX}				= $nAttributeIndex;
				$pAttribute->{NAME} 				= $sAttributeName;
				$pAttribute->{IS_COMPONENT} 	= $::FALSE;
				$pAttribute->{CHOICES} 			= [];
				my $choice;
				foreach $choice (@{$pAttributeDefinition->[$::ABIDX_CHOICES]})
					{
					push (@{$pAttribute->{CHOICES}}, $choice);
					}
				#
				# Add the composed attribute to the available attributes
				#
				if ($bRealComponent)
					{
					$pComponent->{ATTRIBUTES}->{$sAttributeName} = $pAttribute;
					}
				else
					{
					$pComponentHash->{ATTRIBUTES}->{$sAttributeName} = $pAttribute;
					}
				}
			}
		if ($bRealComponent)
			{
			$pComponentHash->{COMPONENTS}->{$pComponent->{NAME}}= $pComponent;
			}
		}
		return $pComponentHash;
	}

############################################################
#
#  FindComponent - decode selected component data
#
#   Params: [0] - component description from the blob
#           [1] - selection array indexed by widget numbers
#           [2] - optional replacement for comma separator
#
#   Returns: [0] - ReturnCode
#            [1] - component choice hash
#
#  Ryszard Zybert  Feb 21 16:13:56 GMT 2000
#
#  Copyright (c) Actinic Software Ltd (2000)
#
############################################################

sub FindComponent
	{
	my $component = shift;
	my $selection = shift;
	my $separator = shift || ',';

	my %Res;
	my @tmp;
	my %hNames;

	$Res{price} = 0,										# price value or array
	$Res{code} = '';										# product reference
	$Res{quantity} = 0;									# associated quantity
	$Res{shipping} = 0;									# shipping opaque data
	$Res{ShipSeparate} = 0;								# Separate shipping flag
	$Res{RetailPrice} = 0;								# retail price

	my $ComponentSwitch = -1;							# For product attributes it stays -1
	my $pAttribute;										# iterator variable

	foreach $pAttribute (@{$component->[$::CBIDX_ATTRIBUTELIST]})		# Check all attributes
		{
		#
		# Predefine often used variables
		#
		my $nWidgetIdx = $pAttribute->[$::ABIDX_WIDGETIDX];	# the widget index
		my $sValue = $selection->[$nWidgetIdx];					# the value of the indexed item

		if( $pAttribute->[$::ABIDX_NAME] eq '' )					# Each component must have an 'on' field
			{
			$ComponentSwitch = $nWidgetIdx;							# This is a 'pseudo-widget' number
			$hNames{COMPONENT} = {"NAME" 	=> $component->[$::CBIDX_NAME],
										 "INDEX"	=> $nWidgetIdx};
			}
		#
		# The indexed item has value (must be component or attribute)
		#
		if( $sValue )
			{
			if ( $sValue =~ /^on/i )									# Component on/off
				{
				if ( $pAttribute->[$::ABIDX_NAME] ne '' )			# Ignore 'pseudo-attributes'
					{
					push @tmp, $selection->[$pAttribute->[$::ABIDX_NAME]];		# Good choice - keep it
					}
				$Res{text} .= $pAttribute->[$::ABIDX_NAME];							# Attribute  name

				if( $pAttribute->[$::ABIDX_NAME] =~ /[^\ ]/ &&
					 $pAttribute->[$::ABIDX_CHOICES]->[$sValue-1] =~ /[^\ ]/ )	# Atribute name and choice name?
					{
					my $nIndex = $sValue - 1;
					$Res{text} .= ': ' . $pAttribute->[$::ABIDX_CHOICES]->[$nIndex];	# Choice description
					$hNames{$nWidgetIdx} = { 	"ATTRIBUTE"	=> $pAttribute->[$::ABIDX_NAME],
														"CHOICE"		=> $pAttribute->[$::ABIDX_CHOICES]->[$nIndex],
														"VALUE"		=> $nIndex + 1 };
					}
				elsif( $pAttribute->[$::ABIDX_NAME] =~ /[^\ ]/ )		# No choices but an attribute name?
					{
					$Res{text} .= ': ' . $pAttribute->[$::ABIDX_NAME];	# Insert attribute name
					}
				$Res{text} .= $separator . ' ';								# Add attribute separator
				$Res{quantity} = $component->[$::CBIDX_QUANTITYUSED];	# Quantity associated with this component
				$Res{price} = $pAttribute->[$::ABIDX_CHOICES];			# Price
				}
			else
				{
				if( $component->[$::CBIDX_ASSOCPRODPRICE] != 1  )		# Not bundled product
					{
					if( $sValue < 1 )
						{
						return ($::SUCCESS, {});								# selection=0 - nothing to do
						}
					if( $sValue - 1 > $#{$pAttribute->[$::ABIDX_CHOICES]} )	# This should never happen (selection out of range)
						{
						my %R;
						$R{text} = ACTINIC::GetPhrase(-1, 2215);
						return ($::FAILURE, %R);
						}
					if( $pAttribute->[$::ABIDX_NAME] ne '' )				# Ignore 'pseudo-attributes'
						{
						push @tmp,$sValue;										# Good choice - keep it
						}
					my $nIndex = $sValue - 1;
					$Res{text} .= $pAttribute->[$::ABIDX_NAME] . ': ' . $pAttribute->[$::ABIDX_CHOICES]->[$nIndex] . $separator . ' ';		# Choice description
					$hNames{$nWidgetIdx} = { 	"ATTRIBUTE"	=> $pAttribute->[$::ABIDX_NAME],
														"CHOICE"		=> $pAttribute->[$::ABIDX_CHOICES]->[$nIndex],
														"VALUE"		=> $nIndex + 1};
					$Res{quantity} = $component->[$::CBIDX_QUANTITYUSED];	# Quantity associated with this component
					}
				else
					{
					$Res{text} .= $pAttribute->[$::ABIDX_NAME] . $separator . ' ';
					$Res{quantity} = $component->[$::CBIDX_QUANTITYUSED];	# Quantity associated with this component
					$hNames{COMPONENT} = {"NAME" 	=> $pAttribute->[$::ABIDX_NAME],
												 "INDEX"	=> $ComponentSwitch};
					}
				}
			}
		#
		# If the component is off
		#
		else
			{
			return ($::SUCCESS, {});
			}
		}
	#
	# Be sure that the name hash is empty if the text is
	#
	if (!$Res{text})										# there were no match?
		{
		%hNames = {};										# be sure that nothing is returned
		}
	$Res{Names} = \%hNames;
	$Res{text} =~ s/[$separator\ ]*$//g;			# Strip trailing comma and spaces
	#
	# Include component name
	#
	if ($Res{text})
		{
		$Res{text} = $component->[$::CBIDX_NAME] . " - " . $Res{text};
		}
	else
		{
		$Res{text} = $component->[$::CBIDX_NAME];
		}
	#
	# Reset quantity if component is not selected
	#
	if ($ComponentSwitch != -1  &&
		 $selection->[$ComponentSwitch] !~ /^on/i )	# Check that there was an 'on' switch
		{
		$Res{quantity} = 0;								# If not - ignore attributes, treat it as not selected
		}
	if( $Res{quantity} == 0 )							# Component not selected
		{
		return ($::SUCCESS,%Res);
		}

	my $range;
	my $rindex;
	my $pPriceHash;
	#
	# Check permutations
	#
	foreach $range (@{$component->[$::CBIDX_PERMUTATIONS]})	# Now check ranges (only specific choices first)
		{
		my $bIsSpecific = $::FALSE;
		my $nMatch = 0;
		if ( $#{$range->[$::PBIDX_CHOICELIST]} != -1 ) 			# count elements in range
			{
			for ( $rindex=0; $rindex<=$#tmp; $rindex++ )			# For all attribute choices
				{
				if ( $range->[$::PBIDX_CHOICELIST]->[$rindex] > 0  &&
					 $range->[$::PBIDX_CHOICELIST]->[$rindex] == $tmp[$rindex] )		# Fit
					{
					$bIsSpecific = $::TRUE;
					$nMatch++;
					}
				elsif ( $range->[$::PBIDX_CHOICELIST]->[$rindex] == -1 )
					{
					$nMatch++;
					}
				}
			}
		if ( $nMatch < $#tmp + 1 )
			{
			next;
			}
		if ( !ref($range->[$::PBIDX_ASSOCIATEDPROD]) &&
			 $range->[$::PBIDX_ASSOCIATEDPROD] =~ /^[\-\+]$/ )	# Range is excluded or out of stock
			{
			my %Prompts;
			$Prompts{'-'} = 296;											# Range is excluded
			$Prompts{'+'} = 297;											# Out of stock
			my ($Status, $sError, $sHTML) = ACTINIC::ReturnToLastPage(7, ACTINIC::GetPhrase(-1, 1962) . ACTINIC::GetPhrase(-1, $Prompts{$range->[$::PBIDX_ASSOCIATEDPROD]}, $component->[0]) . ACTINIC::GetPhrase(-1, 1970) . ACTINIC::GetPhrase(-1, 2053),
																						 ACTINIC::GetPhrase(-1, 208),
																						 $::g_sWebSiteUrl,
																						 $::g_sContentUrl, $::g_pSetupBlob, %::g_InputHash);
			if ($Status != $::SUCCESS)								  	# If even this didn't work - we give up - there is an error
				{
				ACTINIC::ReportError($sError, ACTINIC::GetPath());
				}
			#
			# I can't see why is it needed here but it breaks store branding feature
			# so I have commented out - zmagyar
			#
			#$ACTINIC::AssertIsActive = $::TRUE;					# Cheat here to make sure that PrintPage doesn't call XML parser
			ACTINIC::PrintPage($sHTML, undef, $::TRUE);			# Print warning page and exit
			exit;
			}
		#
		# Check for shipping opaque data
		# This can only be defined for associated products
		#
		if (ref($range->[$::PBIDX_ASSOCIATEDPROD]) eq 'HASH' &&
		    $range->[$::PBIDX_ASSOCIATEDPROD]->{REFERENCE})
			{
			$Res{shipping} = $range->[$::PBIDX_ASSOCIATEDPROD]->{OPAQUE_SHIPPING_DATA};
			$Res{ShipSeparate} = $range->[$::PBIDX_ASSOCIATEDPROD]->{SHIP_SEPARATELY};
			$Res{'RetailPrice'} = $range->[$::PBIDX_ASSOCIATEDPROD]->{PRICE};			# Price
			#
			# Use associated product name?
			#
			if ($component->[$::CBIDX_ASSOCIATEDNAME])
				{
				$Res{text} = $range->[$::PBIDX_ASSOCIATEDPROD]->{NAME};
				}
			#
			# Use associated product tax?
			#
			if ($component->[$::CBIDX_ASSOCIATEDTAX])
				{
				$Res{AssociatedTax} = 1;
				#
				# Copy tax keys
				#
				my $sKey;
				foreach $sKey (keys %{$range->[$::PBIDX_ASSOCIATEDPROD]})
					{
					if ($sKey =~ /TAX_/)
						{
						$Res{$sKey} = $range->[$::PBIDX_ASSOCIATEDPROD]->{$sKey};
						}
					}
				}
			}
		if ( $bIsSpecific )
			{
			if( ref($range->[$::PBIDX_ASSOCIATEDPROD]) eq 'HASH' )	# Whole associated product here
				{
				$Res{code} = $range->[$::PBIDX_ASSOCIATEDPROD]->{REFERENCE};
				#
				# Use associated product name?
				#
				if ($range->[$::PBIDX_ASSOCIATEDNAME])
					{
					$Res{text} = $range->[$::PBIDX_ASSOCIATEDPROD]->{NAME};
					}
				#
				# Store the associated product prices, perhaps we need this later
				# {custom tax)
				#
				$Res{'AssociatedPrice'} = $range->[$::PBIDX_ASSOCIATEDPROD]->{PRICES};
				$Res{'RetailPrice'} = $range->[$::PBIDX_ASSOCIATEDPROD]->{PRICE};
				#
				# Use associated product tax?
				#
				if ($range->[$::PBIDX_ASSOCIATEDTAX])
					{
					$Res{AssociatedTax} = 1;
					#
					# Copy tax keys
					#
					my $sKey;
					foreach $sKey (keys %{$range->[$::PBIDX_ASSOCIATEDPROD]})
						{
						if ($sKey =~ /TAX_/)
							{
							$Res{$sKey} = $range->[$::PBIDX_ASSOCIATEDPROD]->{$sKey};
							}
						}
					}
				#
				# $range->[$::PBIDX_PRICINGMODEL] is the pricing model
				# 0 - component price
				# 1 - associated product price
				# 2 - override price (its own from permutation grid)
				#
				# 1 and 2 is hadled here, the component pricing model is hadled at
				# the end of the next loop
				#
				if( $range->[$::PBIDX_PRICINGMODEL] == 1 )	# We take associated product prices
					{
					$pPriceHash = $range->[$::PBIDX_ASSOCIATEDPROD]->{PRICES};
					}
				elsif( $range->[$::PBIDX_PRICINGMODEL] == 2 )
					{
					$pPriceHash = $range->[$::PBIDX_PRICE];	# Special prices for this component
					}
				}
			else
				{
				if ( $range->[$::PBIDX_ASSOCIATEDPROD] ) 		# Found match, set product code and price
					{
					$Res{code}  = $range->[$::PBIDX_ASSOCIATEDPROD];
					}
				if( $range->[$::PBIDX_PRICE] )
					{
					$pPriceHash = $range->[$::PBIDX_PRICE];
					}
				}
			}
		}

 RANGE:
	foreach $range (@{$component->[$::CBIDX_PERMUTATIONS]})	# Now check ranges (now - all choices)
		{

		if( $#{$range->[$::PBIDX_CHOICELIST]} != -1 ) 			# count elements in range
			{
			for( $rindex=0; $rindex<=$#tmp; $rindex++ )			# For all attribute choices
				{
				if ( $range->[$::PBIDX_CHOICELIST]->[$rindex] > 0 &&
					  $range->[$::PBIDX_CHOICELIST]->[$rindex] != $tmp[$rindex] )		# No fit
					{
					next RANGE;
					}
				}
			}
		if ( !ref($range->[$::PBIDX_ASSOCIATEDPROD]) &&
			 $range->[$::PBIDX_ASSOCIATEDPROD] =~ /^[\-\+]$/ )	# Range is excluded or out of stock
			{
			my %Prompts;
			$Prompts{'-'} = 296;							# Range is excluded
			$Prompts{'+'} = 297;							# Out of stock
			my ($Status, $sError, $sHTML) = ACTINIC::ReturnToLastPage(7,ACTINIC::GetPhrase(-1, 1962) . ACTINIC::GetPhrase(-1, $Prompts{$range->[$::PBIDX_ASSOCIATEDPROD]}, $component->[0]) . ACTINIC::GetPhrase(-1, 1970) . ACTINIC::GetPhrase(-1, 2053),
																						 ACTINIC::GetPhrase(-1, 208),
																						 $::g_sWebSiteUrl,
																						 $::g_sContentUrl, $::g_pSetupBlob, %::g_InputHash);
			if ($Status != $::SUCCESS)					#	 If even this didn't work - we give up - there is an error
				{
				ACTINIC::ReportError($sError, ACTINIC::GetPath());
				}
			#
			# I can't see why is it needed here but it breaks store branding feature
			# so I have commented out - zmagyar
			#
			#$ACTINIC::AssertIsActive = $::TRUE;			# Cheat here to make sure that PrintPage doesn't call XML parser
			ACTINIC::PrintPage($sHTML, undef, $::TRUE);	# Print warning page and exit
			exit;
			}
		#
		# Check for shipping opaque data
		# This can only be defined for associated products
		#
		if (ref($range->[$::PBIDX_ASSOCIATEDPROD]) eq 'HASH' &&
			 $range->[$::PBIDX_ASSOCIATEDPROD]->{REFERENCE})
			{
			$Res{shipping} = $range->[$::PBIDX_ASSOCIATEDPROD]->{OPAQUE_SHIPPING_DATA};
			$Res{ShipSeparate} = $range->[$::PBIDX_ASSOCIATEDPROD]->{SHIP_SEPARATELY};
			#
			# Use associated product name?
			#
			if ($range->[$::PBIDX_ASSOCIATEDNAME])
				{
				$Res{text} = $range->[$::PBIDX_ASSOCIATEDPROD]->{NAME};
				}
			#
			# Store the associated product prices, perhaps we need this later
			# if use associated prod price is checked
			# {custom tax)
			#
			if ($component->[$::CBIDX_ASSOCPRODPRICE])
				{
				$Res{'AssociatedPrice'} = $range->[$::PBIDX_ASSOCIATEDPROD]->{PRICES};
				$Res{'RetailPrice'} = $range->[$::PBIDX_ASSOCIATEDPROD]->{PRICE};
				}
			#
			# Use associated product tax?
			#
			if ($range->[$::PBIDX_ASSOCIATEDTAX])
				{
				$Res{AssociatedTax} = 1;
				#
				# Copy tax keys
				#
				my $sKey;
				foreach $sKey (keys %{$range->[$::PBIDX_ASSOCIATEDPROD]})
					{
					if ($sKey =~ /TAX_/)
						{
						$Res{$sKey} = $range->[$::PBIDX_ASSOCIATEDPROD]->{$sKey};
						}
					}
				}
			}
		if( !$pPriceHash || (keys %$pPriceHash) <= 0)					# Use only if there was no specific choice
			{
			if( ref($range->[$::PBIDX_ASSOCIATEDPROD]) eq 'HASH' )	# Whole associated product here
				{
				$Res{code} = $range->[$::PBIDX_ASSOCIATEDPROD]->{REFERENCE};
				if( $range->[$::PBIDX_PRICINGMODEL] == 1 )				# We take associated product prices
					{
					$pPriceHash = $range->[$::PBIDX_ASSOCIATEDPROD]->{PRICES};
					}
				else
					{
					$pPriceHash = $range->[$::PBIDX_PRICE];				# Special prices for this component
					}
				}
			else
				{
				if( $range->[$::PBIDX_ASSOCIATEDPROD] ) 					# Found match, set product code and price
					{
					$Res{code}  = $range->[$::PBIDX_ASSOCIATEDPROD]
					}
				if( $range->[$::PBIDX_PRICE] )
					{
					$pPriceHash = $range->[$::PBIDX_PRICE]
					}
				}
			}
		}
	if( $pPriceHash && (keys %$pPriceHash) > 0 )
		{
		$Res{price} = $pPriceHash
		}
	return ($::SUCCESS, %Res);
	}

############################################################
#
#  WrapText - simple text wrapper
#  Wraps text to given width on word boundaries.
#  Only spaces are treated as word stops.
#  (No tabs or commas or periods - follow them by a space)
#  If the text contains line breaks they will be preserved.
#  Non breakable words longer than width will be preserved
#  if $preserve option is true. Otherwise long words will be
#  broken with a hyphen.
#  If $preserve is true and $adapt is true long words will cause
#  the width to be extended for all lines.
#
#  Returns a reference to an array containing lines;
#  Arguments:	0	text
#              1  width (if =0 will find minimal width)
#              2  preserve - if true, preserve long words
#              3  adapt - if preserve is true and adapt is true
#                         the width will be increased is necessary
#  Returns:    0 	reference to lines of text
#              1  maximum width actually seen
#
#  Ryszard Zybert  Aug 16 09:46:29 BST 2000
#
#  Copyright (c) Actinic Software Ltd (2000)
#
############################################################

sub WrapText
	{
	my $sText = shift;									# Input text
	my $nWidth = shift;									# wrap width
	my $bPreserve = shift;								# If true, preserve long words
	my $bAdapt = shift;									# Extend width if necessary

	if( $nWidth <= 0 )									# Protect from looping
		{														# Wrap to minimum possible width
		$bPreserve = 1;									# without splitting words
		$nWidth = 0;
		$bAdapt = 1;
		}

	my $pResult;
	my $nRealLength = 0;

	$sText =~ tr/\ /\ /s;								# Squash multiple spaces
	my (@Lines) = split("\r\n",$sText);				# Split into lines

 WRAP:
	foreach (@Lines)										# Split each line
		{
		my $nOffset = 0;									# Current offest
		my $sLine = $_ . ' ';							# Terminate each line
		my $nTotlen = length($sLine);
		#
		# Split on the last space within the wanted width
		# and push the result into output list
		#
		while ( $nOffset < $nTotlen )
			{
			my $nExtra = 0;														  	# Number of added characters
			my $sExtra = "";															# Extra characters
			my $nLen = rindex(substr($sLine,$nOffset,$nWidth),' ');		# Find last space
			if( $nLen<0 )																# Word too long
				{
				if( $bPreserve )														# Preserve long words
					{
					$nLen = index(substr($sLine,$nOffset),' ');				# No breaking - get the word
					if( $bAdapt )														# If adapt, change the width
						{																	# and start again
						$pResult = [];
						$nWidth = $nLen + 1;
						goto WRAP;
						}
					}
				else																		# Break it
					{
					$nLen = $nWidth - 1;												# Make space for hyphen
					$nExtra = 1;														# Remember that we added it
					$sExtra = '-';														# Add hyphen
					}
				}
			push @{$pResult},substr($sLine,$nOffset,$nLen) . $sExtra;	# Store the line
			$nOffset += $nLen + 1;													# Move cursor
			#
			# Keep track of longest line
			#
			$nRealLength = $nLen+$nExtra if $nLen+$nExtra > $nRealLength;
			}
		}
	return ($pResult,$nRealLength);
	}


############################################################
#  SetCartCookie - generate cart content cookie
#
#  Returns: cookie string
#
#  Zoltan Magyar  24/03/2001
#
############################################################

sub GenerateCartCookie
	{
	#
	#Set the default cookie content
	#
	my $sCookie =  "CART_TOTAL\t0\tCART_COUNT\t0\n";
	#
	# Read the shopping cart
	#
	my @Response = $::Session->GetCartObject();
	if ($Response[0] != $::SUCCESS)					# general error
		{
		return ("CART_CONTENT=". ACTINIC::EncodeText2($sCookie,0));	# empty cart so return empty string
		}
	my $pCartObject = $Response[2];

	my $pCartList = $pCartObject->GetCartList();
	#
	# Get item count
	#
	my $nCount = $pCartObject->CountQuantities();
	if ($nCount <= 0)
		{
		return ("CART_CONTENT=". ACTINIC::EncodeText2($sCookie,0));	# empty cart so return empty string
		}
	#
	# Summarize order
	#
	 @Response = $pCartObject->SummarizeOrder($::TRUE);# calculate the order total
	if ($Response[0] != $::SUCCESS)
		{
		return ("CART_CONTENT=". ACTINIC::EncodeText2($sCookie,0));
		}
	my $nTotal = $Response[6];
		my $vatStr ="tx=" . $Response[4] . "sh=" . $Response[3];
	#
	# Format price
	#
	@Response = ActinicOrder::FormatPrice($nTotal, $::TRUE, $::g_pCatalogBlob);	# Format current total
	if ($Response[0] != $::SUCCESS)
		{
		return ("CART_CONTENT=". ACTINIC::EncodeText2($sCookie,0));
		}
	my $sTotal = $Response[2];
	#
	# Create Cookie string
	#
	
	$sCookie =  "CART_TOTAL\t" . ACTINIC::EncodeText2($sTotal) . "\tCART_COUNT\t" . ACTINIC::EncodeText2($nCount) . "\t" . $vatStr . $pCartObject->fullCartSummary() . "\n";

	#$sCookie =  "CART_TOTAL\t" . ACTINIC::EncodeText2($sTotal) . "\tCART_COUNT\t" . ACTINIC::EncodeText2($nCount) . "\n";
	$sCookie = "CART_CONTENT=" . ACTINIC::EncodeText2($sCookie,0);
	return($sCookie);
	}

##############################################################################################################
#
# Cart Processing - End
#
##############################################################################################################

##############################################################################################################
#
# Financial Processing - Begin
#
##############################################################################################################

#######################################################
#
# ProductToOrderDetailTaxOpaqueData - Adjust tax opaque data
#
# Params:	$sTaxID				- tax ID (TAX_1 or TAX_2)
#				$nUnitCost			- the unit price
#				$sTaxBand			- the opaque data
#				$nRetailPrice		- the unit retail price
#
#
# Returns:	(adjusted tax opaque data)
#
#######################################################

sub ProductToOrderDetailTaxOpaqueData
	{
	my ($sTaxID, $nUnitCost, $sTaxBand, $nRetailPrice) = @_;
	my ($nTaxBandID, $nTaxRate, $nCustomRate, $sBandName) = split /=/, $sTaxBand;

	my $nTaxID;
	#
	# Find out if this tax is applicable for this zone
	#
	if(defined $$::g_pTaxSetupBlob{$sTaxID})
		{
		$nTaxID = $$::g_pTaxSetupBlob{$sTaxID}{ID};
		}
	else
		{
		return('0=0=0==');
		}
	#
	# Adjust any custom tax data
	#
	if($nTaxBandID == $ActinicOrder::CUSTOM)
		{
		$sTaxBand = AdjustCustomTaxOpaqueData($nTaxID, $nUnitCost, $sTaxBand, $nRetailPrice);
		}
	#
	# Append the band name
	#
	if(defined $$::g_pTaxesBlob{$nTaxID})
		{
		$sBandName = $$::g_pTaxesBlob{$nTaxID}{'BANDS'}{$nTaxBandID}{'BAND_NAME'};
		$sTaxBand .= "$sBandName=";
		}
	else
		{
		return('0=0=0==');
		}
	return($sTaxBand);
	}

#######################################################
#
# AdjustCustomTaxOpaqueData - Adjust tax opaque data
#
# Params:	$nTaxID				- the tax ID
#				$nUnitCost			- the unit price
#				$sTaxBand			- the quantity
#				$nRetailPrice		- the unit retail price
#
#
# Returns:	(adjusted custom tax opaque data)
#
#######################################################

sub AdjustCustomTaxOpaqueData
	{
	my ($nTaxID, $nUnitCost, $sTaxBand, $nRetailPrice) = @_;
	my ($nTaxBand, $nTaxRate, $nCustomRate, $sBandName) = split /=/, $sTaxBand;
	my $sBandData;
	if($nRetailPrice != 0)								# protect against divide by 0
		{
		#
		# calculate the percentage rate
		#
		$nTaxRate = ($nCustomRate / $nRetailPrice) * 100 * 100;
		$nTaxRate = RoundTax($nTaxRate,
			$$::g_pTaxesBlob{$nTaxID}{'ROUND_RULE'});
		#
		# if there's a discount, adjust the flat rate value
		#
		if($nUnitCost != $nRetailPrice)
			{
			$nCustomRate = $nUnitCost / $nRetailPrice * $nCustomRate;
			$nCustomRate = RoundTax($nCustomRate, $$::g_pTaxesBlob{$nTaxID}{'ROUND_RULE'});
			}
		#
		# reformat the opaque data
		#
		$sBandData = sprintf("%d=%d=%d=", $nTaxBand, $nTaxRate, $nCustomRate);
		return($sBandData);
		}
	return($sTaxBand);
	}

#######################################################
#
# PrepareProductTaxOpaqueData - prepare the product tax opaque data
#
# Input:	$pProduct					- reference to the product hash
#			$sPrice						- schedule price
#			$nCustomTaxBase			- the retail price custom tax is applicable to
#			$bTreatCustomAsExempt	- whether custom tax should be treated as exempt
#			$nOverrideBandID			- override fixed tax band ID
#
# Returns:	0 - $::SUCCESS or $::FAILURE
#				1 - undef or error message
#				2 - tax opaque data
#
#######################################################

sub PrepareProductTaxOpaqueData
	{
	my($pProduct, $sPrice, $nCustomTaxBase, $bTreatCustomAsExempt, $nOverrideBandID) = @_;
	my ($nTaxID, $sOpaqueData);
	foreach $nTaxID (sort keys %$::g_pTaxesBlob)	# go through all the taxes
		{
		my ($sTaxBand, $nTaxBandID, $nTaxRate, $nCustomRate, $sBandName);
		#
		# See if we want to override the bands
		#
		if(defined $nOverrideBandID)
			{
			$nTaxBandID = $nOverrideBandID;
			if($nTaxBandID == $ActinicOrder::PRORATA)		# pro-rata
				{
				$sTaxBand = '5=0=0=';
				}
			elsif($nTaxBandID == $ActinicOrder::EXEMPT)	# exempt
				{
				$sTaxBand = '1=0=0=';
				}
			elsif($nTaxBandID == $ActinicOrder::ZERO)		# zero-rated
				{
				$sTaxBand = '0=0=0=';
				}
			}
		else
			{
			$sTaxBand = $$pProduct{'TAX_' . $nTaxID};					# get the tax band from the product hash
			($nTaxBandID, $nTaxRate, $nCustomRate, $sBandName) =
				split /=/, $sTaxBand;										# split into components

			if($nTaxBandID == $ActinicOrder::CUSTOM)					# adjust custom tax if necessary
				{
				if($bTreatCustomAsExempt)									# if we're treating custom tax as exempt
					{
					$sTaxBand = '1=0=0=';									# set tax band to exempt
					$nTaxBandID = $ActinicOrder::EXEMPT;				# set band ID to exempt
					}
				else
					{
					$sTaxBand = ActinicOrder::AdjustCustomTaxOpaqueData($nTaxID, $sPrice, $sTaxBand, $nCustomTaxBase);
					}
				}
			}
		#
		# Get the band name from the taxes hash
		#
		if(defined $$::g_pTaxesBlob{$nTaxID})
			{
			$sBandName = $$::g_pTaxesBlob{$nTaxID}{'BANDS'}{$nTaxBandID}{'BAND_NAME'};
			}

		$sOpaqueData .= "$nTaxID\t$sTaxBand$sBandName=\n";
		}
	return($::SUCCESS, '', $sOpaqueData);
	}

#######################################################
#
# CalculateDefaultTax - calculate the default tax on the 
#	specified total and tax bands.  The tax calculation is done
#	by ignoring all known tax info (default tax).
#
# This function is only a wrapper for CalculateTax
#
# Zoltan Magyar - 21/01/2003
#
#######################################################

sub CalculateDefaultTax
	{
	#
	# If tax info is not defined yet then the actual and the default tax is the same
	# 
	if (!TaxIsKnown())
		{
		return(CalculateTax(@_));
		}
	my %SavedTaxInfo = %::g_TaxInfo;					# save the tax info
	%::g_TaxInfo = {};									# reset tax
	my @Response = CalculateTax(@_);					# calculate tax by using the default values
	%::g_TaxInfo = %SavedTaxInfo;						# restore tax info
	return @Response;
	}
	
#######################################################
#
# CalculateTax - calculate the tax on the specified
#	total and tax bands.  take into consideration
#	the tax rounding.
#
# Params:	$nUnitCost			- the unit price
#				$nQuantity			- the quantity
#				$aTaxBands[0]		- the tax 1 band ID
#				$aTaxBands[1]		- the tax 2 band ID
#				$nRetailPrice		- the unit retail price
#
#				if ProRata tax is applicable, the following
#				are also supplied:
#
#				$nProductTotal		- the product total
#				$aProductTax[0]	- tax 1 on products
#				$aProductTax[1]	- tax 2 on products
#
# Returns:	0 - status
#				1 - error message
#				2 - tax 1
#				3 - tax 2
#
#######################################################

sub CalculateTax
	{
	if ($#_ != 4 && $#_ != 7)
		{
		return ($::FAILURE, ACTINIC::GetPhrase(-1, 12, 'CalculateTax'), 0, 0);
		}
	my ($nValue, @aProductTax, $nProductTotal, @aTaxes, $nTax);
	my ($nUnitCost, $nQuantity) = @_[0,1];
	my (@aTaxBands) = @_[2,3];
	my $nRetailPrice = $_[4];
	#
	# get the product taxes and totals if this could be pro-rata tax
	#
	if ($#_ == 7)
		{
		$nProductTotal = $_[5];
		@aProductTax = @_[6,7];
		}
	#
	# check the unit cost or quantity isn't 0
	#
	if ($nUnitCost == 0 || $nQuantity == 0)
		{
		return ($::SUCCESS, '', 0, 0);							# 100% of nothing is nothing
		}
	foreach $nTax (0 .. 1)
		{
		my $sTaxKey = 'TAX_' . ($nTax + 1);
		my $sExemptKey = 'EXEMPT' . ($nTax + 1);				# key for user exemption data

		my $bLocationTaxable = defined $$::g_pTaxSetupBlob{$sTaxKey};
		#
		# Clear any non-allowed exemption
		#
		if($bLocationTaxable &&										# if the tax is levied
			!$$::g_pTaxSetupBlob{$sTaxKey}{ALLOW_EXEMPT})	# but not exemptable
			{
			$::g_TaxInfo{$sExemptKey} = 0;						# make sure they can't exempt themselves
			}

		my ($nBandID, $nPercent, $nFlatRate, $sBandName) = split /=/, $aTaxBands[$nTax];
		if ($nBandID == $ActinicOrder::ZERO		||	# zero tax
			 $nBandID == $ActinicOrder::EXEMPT	|| # tax exempt
			 !$bLocationTaxable						||	# location exempt
			 ($$::g_pTaxSetupBlob{$sTaxKey}{ALLOW_EXEMPT} && $::g_TaxInfo{$sExemptKey}))	# user exempt
			{
			$aTaxes[$nTax] = 0;
			}
		elsif ($nBandID == $ActinicOrder::PRORATA)			# pro-rata
			{
			if ($nProductTotal == 0 ||								# avoid divide by zero
				$aProductTax[$nTax] == 0)							# there's no tax on this order
				{
				$aTaxes[$nTax] = 0;
				$aTaxes[$nTax + 2] = 0;
				}
			else															# need to calculate the tax
				{
				#
				# pro-rata only applies to shipping so the quantity is always 1
				# so we just use the unit cost
				#
				$aTaxes[$nTax] = $nUnitCost * $aProductTax[$nTax] / $nProductTotal;
				$aTaxes[$nTax + 2] = $aTaxes[$nTax];			# keep a copy of the un-rounded value
				#
				# if we aren't rounding per order, round the tax
				#
				if ($$::g_pTaxSetupBlob{$sTaxKey}{'ROUND_GROUP'} != $ActinicOrder::ROUNDPERORDER)
					{
					$aTaxes[$nTax] = RoundTax($aTaxes[$nTax],
						$$::g_pTaxSetupBlob{$sTaxKey}{'ROUND_RULE'});
					}
				}
			}
		elsif ($nBandID == $ActinicOrder::CUSTOM)				# custom tax
			{
			if ($nRetailPrice == 0)									# avoid divide by zero
				{
				$aTaxes[$nTax] = 0;
				$aTaxes[$nTax + 2] = 0;
				}
			elsif($nUnitCost == $nRetailPrice)					# no need to calculate the tax
				{
				$aTaxes[$nTax] = $nFlatRate * $nQuantity;
				$aTaxes[$nTax + 2] = $aTaxes[$nTax];			# keep a copy of the un-rounded value
				}
			else															# need to calculate the tax
				{
				#
				# calculate the tax rate
				#
				my $nTaxRate = ($nFlatRate / $nRetailPrice) * 100 * 100;
				$nTaxRate = RoundTax($nTaxRate,
						$$::g_pTaxSetupBlob{$sTaxKey}{'ROUND_RULE'}) / 100;
				#
				# calculate the tax
				#
				$aTaxes[$nTax + 2] = ($nUnitCost * $nTaxRate / 100)  * $nQuantity;
				$aTaxes[$nTax] = RoundTax($nUnitCost * $nTaxRate / 100,
										$$::g_pTaxSetupBlob{$sTaxKey}{'ROUND_RULE'})  * $nQuantity;
				}
			}
		else
			{
			my $nBandRate = $$::g_pTaxSetupBlob{$sTaxKey}{'BANDS'}{$nBandID}{'BAND_RATE'};
			my $nLowThreshold = $$::g_pTaxSetupBlob{$sTaxKey}{'LOW_THRESHOLD'};
			my $nHighThreshold = $$::g_pTaxSetupBlob{$sTaxKey}{'HIGH_THRESHOLD'};
			my $nUnRoundedValue;
			#
			# Multiply the unit cost by quantity unless we are rounding per item
			#
			if ($$::g_pTaxSetupBlob{$sTaxKey}{'ROUND_GROUP'} != $ActinicOrder::ROUNDPERITEM)
				{
				$nValue = $nUnitCost * $nQuantity;
				}
			else
				{
				$nValue = $nUnitCost;
				}
			$nUnRoundedValue = $nValue;
			#
			# add other taxes to the value if this tax is cumulative
			#
			if ($$::g_pTaxSetupBlob{$sTaxKey}{'TAX_ON_TAX'})
				{
				my $i;
				#
				# add previously calculated taxes
				#
				for ($i = $nTax - 1; $i >= 0; $i--)
					{
					#
					# if we are rounding per item, divide previous taxes by quantity
					# before adding to the unit cost
					#
					if ($$::g_pTaxSetupBlob{$sTaxKey}{'ROUND_GROUP'} == $ActinicOrder::ROUNDPERITEM)
						{
						$nValue += $aTaxes[$i] / $nQuantity;
						$nUnRoundedValue += $aTaxes[$i + 2] / $nQuantity;
						}
					else
						{
						$nValue += $aTaxes[$i];
						$nUnRoundedValue += $aTaxes[$i + 2];
						}
					}
				}
			#
			# Calculate the tax on the absolute amount if levied
			#
			my $nNegativeMultiplier = ($nUnitCost < 0) ? -1 : 1;
			$nUnitCost *= $nNegativeMultiplier;		# convert to positive if required
			if	($nBandRate > 0 &&					# if the tax is levied
				$nUnitCost > $nLowThreshold &&		# and the amount exceeds the threshold
				($nHighThreshold == 0 || $nHighThreshold > $nUnitCost))# and it's less than the high threshold if specified
				{
				$aTaxes[$nTax] = $nValue * $nBandRate / $ActinicOrder::PERCENTOFFSET;
				$aTaxes[$nTax + 2] = $nUnRoundedValue * $nBandRate / $ActinicOrder::PERCENTOFFSET; 
				$nUnitCost *= $nNegativeMultiplier;		# convert to negative if required
				#
				# if we aren't rounding per order, round the tax
				#
				if ($$::g_pTaxSetupBlob{$sTaxKey}{'ROUND_GROUP'} != $ActinicOrder::ROUNDPERORDER)
					{
					#
					# round the tax on the item or line
					#
					$aTaxes[$nTax] = RoundTax($aTaxes[$nTax],
						$$::g_pTaxSetupBlob{$sTaxKey}{'ROUND_RULE'});
					}
				#
				# if we are rounding per item, multiply by quantity
				#
				if ($$::g_pTaxSetupBlob{$sTaxKey}{'ROUND_GROUP'} == $ActinicOrder::ROUNDPERITEM)
					{
					$aTaxes[$nTax] *= $nQuantity;
					$aTaxes[$nTax + 2] *= $nQuantity; 
					}
				}
			else
				{
				$aTaxes[$nTax] = 0;
				$aTaxes[$nTax + 2] = 0;
				}
			}
		}
	return ($::SUCCESS, '', @aTaxes);
	}

#######################################################
#
# RoundTax - round the value based on the specified
#	rounding rule
#
# Params:	0 - value
#				1 - the rounding rule
#                                				1.0     1.1     1.4     1.5     1.6
#  ActinicOrder::TRUNCATION                    1       1       1       1       1
#  ActinicOrder::SCIENTIFIC_DOWN               1       1       1       1       2
#  ActinicOrder::SCIENTIFIC_NORMAL             1       1       1       2       2
#  ActinicOrder::CEILING                       1       2       2       2       2
#
# Returns:	0 - rounded value
#
#######################################################

sub RoundTax
	{
#? ACTINIC::ASSERT($#_ == 1, "RoundTax has an invalid argument count ($#_).", __LINE__, __FILE__);
	my ($nValue, $eRule) = @_;
#? ACTINIC::ASSERT($eRule == $ActinicOrder::TRUNCATION ||
#?		$eRule == $ActinicOrder::SCIENTIFIC_DOWN ||
#?		$eRule == $ActinicOrder::SCIENTIFIC_NORMAL ||
#?		$eRule == $ActinicOrder::CEILING,
#?		"Invalid tax rounding rule($eRule).", __LINE__, __FILE__);
	my $bNegative = $::FALSE;
	#
	# Convert -ve values for rounding
	#
	if($nValue < 0)
		{
		$bNegative = $::TRUE;
		$nValue *= -1;
		}
	#
	# Do the rounding on the positive value
	#
	if ($eRule == $ActinicOrder::TRUNCATION)
		{
		$nValue = int $nValue;
		}
	elsif ($eRule == $ActinicOrder::SCIENTIFIC_DOWN)
		{
		$nValue = int ($nValue + 0.499999);
		}
	elsif ($eRule == $ActinicOrder::SCIENTIFIC_NORMAL)
		{
		$nValue = int ($nValue + 0.5);
		}
	elsif ($eRule == $ActinicOrder::CEILING)
		{
		$nValue = int ($nValue + 0.999999);
		}
	#
	# Convert back to negative
	#
	if($bNegative)											# if the value was negative
		{
		$nValue *= -1;										# convert back
		}

	return ($nValue);
	}

#######################################################
#
# RoundScientific - round the value based on the scientific
#	rounding rule
#
# Params:	0 - value
#                                				1.0     1.1     1.4     1.5     1.6
#  ActinicOrder::SCIENTIFIC_NORMAL             1       1       1       2       2
#
# Returns:	0 - rounded value
#
#######################################################

sub RoundScientific
	{
	return(RoundTax(@_, $ActinicOrder::SCIENTIFIC_NORMAL));
	}

#######################################################
#
# FormatPrice - format the price into a text string
#
# Params:	0 - the price in catalog format
#				1 - boolean indicating
#					whether to include the monetary symbol.
#					if $::FALSE, leave out the symbol.
#				2 - a reference to the currency table (NOTE: the
#					catalog blob can be inserted here, since
#					it contains the currency table)
#
# Returns:	0 - status
#				1 - error message
#				2 - formatted price
#
#######################################################
#   WARNING WARNING WARNING WARNING WARNING WARNING
#######################################################
#
# Any modification of this function's interface or
# basic functionality have to be reflected in the
# shipping plug in!!!
# 20 Feb 2002 gmenyhert
#
#######################################################
#   WARNING WARNING WARNING WARNING WARNING WARNING
#######################################################

sub FormatPrice
	{
	if ($#_ != 2)
		{
		return ($::FAILURE, ACTINIC::GetPhrase(-1, 12, 'FormatPrice'), 0, 0);
		}

	my ($nPrice, $bSymbolPresent, $pCurrencyTable);
	($nPrice, $bSymbolPresent, $pCurrencyTable) = @_;
	#
	# format the price in the Catalog currency
	#
	my($nStatus, $sMessage, $sFormattedPrice) = FormatSinglePrice($nPrice, $bSymbolPresent, $pCurrencyTable);
	#
	# format the price in alternate currency if required
	#
	if($$::g_pSetupBlob{'PRICES_DISPLAYED'} && $$::g_pSetupBlob{'ALT_CURRENCY_PRICES'})
		{
		my ($sAltPricePrice, $nAltPricePrice, $sAltCurrencyIntlSymbol);
		#
		# calculate the other currency price
		#
		$sAltCurrencyIntlSymbol = $$::g_pSetupBlob{'ALT_CURRENCY_INTL_SYMBOL'};
		$nAltPricePrice = $nPrice * $$pCurrencyTable{$sAltCurrencyIntlSymbol}{'EXCH_RATE'};
		#
		# Fix up the differences occured by different decimal digits
		#
		my $nAdjustment = 10 ** ($$pCurrencyTable{ICURRDIGITS} - $$pCurrencyTable{$sAltCurrencyIntlSymbol}{ICURRDIGITS});
		$nAltPricePrice /= $nAdjustment;
		#
		# round the price
		#
		$nAltPricePrice = RoundTax($nAltPricePrice, $ActinicOrder::SCIENTIFIC_NORMAL);
		#
		# format the price
		#
		($nStatus, $sMessage, $sAltPricePrice) =
			FormatSinglePrice($nAltPricePrice, $bSymbolPresent, $$pCurrencyTable{$sAltCurrencyIntlSymbol}, $::TRUE);
		#
		# format the catalog and other currency prices
		#
		$sFormattedPrice = sprintf($$::g_pSetupBlob{'EURO_FORMAT'}, $sFormattedPrice, $sAltPricePrice);
		}
	return ($::SUCCESS, '', $sFormattedPrice, 0);
	}

#######################################################
#
# FormatSinglePrice - format the price into a text string
#
# Params:	0 - the price in catalog format
#				1 - boolean indicating
#					whether to include the monetary symbol.
#					if $::FALSE, leave out the symbol.
#				2 - a reference to the currency table (NOTE: the
#					catalog blob can be inserted here, since
#					it contains the currency table)
#				3 - optional flag to say whether the alternate price symbol
#					should be used - if missing defaults to false
#
# Returns:	0 - status
#				1 - error message
#				2 - formatted price
#
#######################################################
#   WARNING WARNING WARNING WARNING WARNING WARNING
#######################################################
#
# Any modification of this function's interface or
# basic functionality have to be reflected in the
# shipping plug in!!!
# 20 Feb 2002 gmenyhert
#
#######################################################
#   WARNING WARNING WARNING WARNING WARNING WARNING
#######################################################

sub FormatSinglePrice
	{
	if ($#_ != 2 && $#_ != 3)
		{
		return ($::FAILURE, ACTINIC::GetPhrase(-1, 12, 'FormatSinglePrice'), 0, 0);
		}

	my ($nPrice, $bSymbolPresent, $pCurrencyTable, $bUseOtherCurrencySymbol) = @_;

	my ($nNumDigits, $nGrouping, $sDecimal, $sThousand, $eNegOrder, $ePosOrder, $sCurSymbol);
	$nNumDigits = $$pCurrencyTable{"ICURRDIGITS"};	# read the currency format values
	$nGrouping = $$pCurrencyTable{"IMONGROUPING"};
	$sDecimal = $$pCurrencyTable{"SMONDECIMALSEP"};
	$sThousand = $$pCurrencyTable{"SMONTHOUSANDSEP"};
	$eNegOrder = $$pCurrencyTable{"INEGCURR"};
	$ePosOrder = $$pCurrencyTable{"ICURRENCY"};

	if (defined $::USEINTLCURRENCYSYMBOL &&
		 $::USEINTLCURRENCYSYMBOL == $::TRUE)
		{
		$sCurSymbol = $$pCurrencyTable{"SINTLSYMBOLS"};
		}
	else
		{
		if ($#_ == 3 && $bUseOtherCurrencySymbol)
			{
			$sCurSymbol = $$pCurrencyTable{"ALT_CURRENCY_SYMBOL"};
			}
		else
			{
			$sCurSymbol = $$pCurrencyTable{"SCURRENCY"};
			}
		}
	if (!$bSymbolPresent)								# if the symbol should not be displayed
		{
		$sCurSymbol = '';									# clear the symbol
		}

	#
	# parse currency value
	#
	my ($dPrice, $nFraction, $nWholePart, $bNegative, $dRoundAdjustment);

	$bNegative = ($nPrice < 0);						# get the negative status of the value
	$nPrice = abs $nPrice;								# treat everything as positive for now
	$nPrice = int($nPrice + 0.5);						# be sure that Actinic price is integer
	$dPrice = $nPrice / (10 ** $nNumDigits);		# convert the internal format to the currency float
	$nWholePart = int $dPrice;							# get the whole dollar amount
	$dRoundAdjustment = 10 ** (-1 * ($nNumDigits + 1));
	$nFraction = int (($dPrice - $nWholePart + $dRoundAdjustment) * (10 ** $nNumDigits)); # get the fractional portion

	################
	# format number portion - neglect negative status and currency symbol
	################

	#
	# format the whole part
	#
	my ($nCount, @nWholeParts, $sPart, $nSafeOffset, $nSafeGroup);
	if ($nGrouping != 0 && $nGrouping ne "")
		{
		for ($nCount = (length $nWholePart) - $nGrouping; $nCount > (-1 * $nGrouping); $nCount -= $nGrouping)
			{
			if ($nCount < 0)
				{
				$nSafeOffset = 0;							# make sure you don't have a negative index
				$nSafeGroup = $nGrouping + $nCount;	# compensate in the string length
				}
			else
				{
				$nSafeOffset = $nCount;
				$nSafeGroup = $nGrouping;
				}

			$sPart = substr ($nWholePart, $nSafeOffset, $nSafeGroup); # strip this group of digits (3 for US$)
			push (@nWholeParts, $sPart);				# store the whole part of the price in "thousands" chunks separated
			}
		}

	my ($sFormattedPrice, $sFormat);
	$sFormattedPrice = '';
	while (scalar @nWholeParts > 0)					# stitch those thousands chunks back together with the proper separator
		{
		$sFormattedPrice .= (pop @nWholeParts) . $sThousand;
		}
	$sFormattedPrice = substr ($sFormattedPrice, 0,
										(length $sFormattedPrice) - (length $sThousand)); # strip the trailing separator

	#
	# add the decimal portion to the list if it exists
	#
	if ($nNumDigits > 0)
		{
		$sFormat = '%s%s%' . $nNumDigits . "." . $nNumDigits . "d";
		$sFormattedPrice = sprintf ($sFormat, $sFormattedPrice, $sDecimal, $nFraction);
		}
	#
	# add the symbol (if there is one), and format the negative values
	#
	if ($bNegative)
		{
		if ($eNegOrder == 0)
			{
			$sFormattedPrice = "(".$sCurSymbol.$sFormattedPrice.")";
			}
		elsif ($eNegOrder == 1)
			{
			$sFormattedPrice = "-".$sCurSymbol.$sFormattedPrice;
			}
		elsif ($eNegOrder == 2)
			{
			$sFormattedPrice = $sCurSymbol."-".$sFormattedPrice;
			}
		elsif ($eNegOrder == 3)
			{
			$sFormattedPrice = $sCurSymbol.$sFormattedPrice."-";
			}
		elsif ($eNegOrder == 4)
			{
			$sFormattedPrice = "(".$sFormattedPrice.$sCurSymbol.")";
			}
		elsif ($eNegOrder == 5)
			{
			$sFormattedPrice = "-".$sFormattedPrice.$sCurSymbol;
			}
		elsif ($eNegOrder == 6)
			{
			$sFormattedPrice = $sFormattedPrice."-".$sCurSymbol;
			}
		elsif ($eNegOrder == 7)
			{
			$sFormattedPrice = $sFormattedPrice.$sCurSymbol."-";
			}
		elsif ($eNegOrder == 8)
			{
			$sFormattedPrice = "-".$sFormattedPrice." ".$sCurSymbol;
			}
		elsif ($eNegOrder == 9)
			{
			$sFormattedPrice = "-".$sCurSymbol." ".$sFormattedPrice;
			}
		elsif ($eNegOrder == 10)
			{
			$sFormattedPrice = $sFormattedPrice." ".$sCurSymbol."-";
			}
		elsif ($eNegOrder == 11)
			{
			$sFormattedPrice = $sCurSymbol." ".$sFormattedPrice."-";
			}
		elsif ($eNegOrder == 12)
			{
			$sFormattedPrice = $sCurSymbol." -".$sFormattedPrice;
			}
		elsif ($eNegOrder == 13)
			{
			$sFormattedPrice = $sFormattedPrice."- ".$sCurSymbol;
			}
		elsif ($eNegOrder == 14)
			{
			$sFormattedPrice = "(".$sCurSymbol." ".$sFormattedPrice.")";
			}
		elsif ($eNegOrder == 15)
			{
			$sFormattedPrice = "(".$sFormattedPrice." ".$sCurSymbol.")";
			}
		}
	else
		{
		if ($ePosOrder == 0)
			{
			$sFormattedPrice = $sCurSymbol.$sFormattedPrice;
			}
		elsif ($ePosOrder == 1)
			{
			$sFormattedPrice .= $sCurSymbol;
			}
		elsif ($ePosOrder == 2)
			{
			$sFormattedPrice = $sCurSymbol." ".$sFormattedPrice;
			}
		elsif ($ePosOrder == 3)
			{
			$sFormattedPrice .= " ".$sCurSymbol;
			}
		}
	return ($::SUCCESS, '', $sFormattedPrice, 0);
	}

#######################################################
#
# FormatTaxRate - format the tax rate into a text string
#
# Params:	0 - the rate in catalog format (ie X100)
#
# Returns:	0 - the tax rate as a string
#
#######################################################

sub FormatTaxRate
	{
	if ($#_ != 0)
		{
		return ($::FAILURE, ACTINIC::GetPhrase(-1, 12, 'FormatTaxRate'), 0, 0);
		}

	my ($nRate) = @_;
	my ($nIntegerRate, $nDecimalRate);
	#
	# Calculate the real integer part
	#
	$nIntegerRate = $nRate / 100;
	#
	# Convert rate * 100 to rate * 10000 to get an integer
	#
	$nDecimalRate = (($nRate * 100) + 0.2) % 10000;
	if ($nDecimalRate)
		{
		while (!($nDecimalRate % 10))
			{
			$nDecimalRate /= 10;
			}
		return (sprintf('%d.%d', $nIntegerRate, $nDecimalRate));
		}
	else
		{
		return('' . $nIntegerRate);
		}
	}

#######################################################
#
# FormatCompletePrice - format the price with the tax
#	message into a text string including the HTML
#	and labels
#
# Params:	$_[0] - the price in catalog format
#				$_[1] - the tax 1 band
#				$_[2] - the tax 2 band
#				$_[3] - the retail price in catalog format
#				$_[4] - prompt id for label (optional)
#				$_[5] - string for label (optional)
#				$_[6] - string for quantity description (optional)
#
# Returns:	($ReturnCode, $Error, $sFormattedText, 0)
#				if $ReturnCode = $::FAILURE, the operation failed
#					for the reason specified in $Error
#				Otherwise everything is OK
#
#######################################################

sub FormatCompletePrice
	{
	if (!defined $_[0] || !defined$_[1])
		{
		return ($::FAILURE, ACTINIC::GetPhrase(-1, 12, 'FormatCompletePrice'), 0, 0);
		}
	my ($nPrice, $nTax1Band, $nTax2Band, $nRetailPrice, $nLabelPrompt, $sLabelPrompt, $sQuantityPrompt, $nTax1, $nTax2) = @_;
	my ($sLine, $Status, $Message, @Response);
	$sLine = "";
	#
	# Get the price template
	#
	@Response = GetPriceTemplate();
	if($Response[0] != $::SUCCESS)
		{
		return(@Response);
		}
	my $rPriceTemplate = $Response[2];

	if ($$::g_pSetupBlob{"PRICES_DISPLAYED"} &&			# if the prices can be displayed
		 $nPrice != 0)										# and the item is not free
		{
		my ($sPrice);
		#
		# if we are only displaying tax inclusive prices
		# add the tax to the price
		#
		if ($::g_pSetupBlob->{TAX_INCLUSIVE_PRICES} && !$::g_pSetupBlob->{TAX_EXCLUSIVE_PRICES})
			{
			#
			# See if the tax is already calculated
			#
			if ((defined $nTax1 && $nTax1 > 0) ||
				 (defined $nTax2 && $nTax2 > 0))
				{
				$nPrice += $nTax1 + $nTax2;
				}
			else
				{
				@Response = ActinicOrder::CalculateTax($nPrice, 1, $nTax1Band, $nTax2Band, $nRetailPrice);
				if($Response[0] != $::SUCCESS)
					{
					return (@Response);
					}
				$nPrice = $nPrice + $Response[2] + $Response[3];
				}
			}

		@Response = ActinicOrder::FormatPrice($nPrice, $::TRUE, $::g_pCatalogBlob);	# format the price
		($Status, $Message, $sPrice) = @Response;
		if ($Status != $::SUCCESS)
			{
			return (@Response);
			}

		@Response = ACTINIC::EncodeText($sPrice, $::TRUE, $::TRUE);	# convert special characters to their hex equivalent
		($Status, $sPrice) = @Response;
		if ($Status != $::SUCCESS)
			{
			return (@Response);
			}

		my ($sExcludes);
		@Response = FormatTaxMessage($nTax1Band, $nTax2Band, $nPrice, $nRetailPrice, $nTax1, $nTax2); # get the tax treatment
		($Status, $Message, $sExcludes) = @Response; # build the "Excludes Tax" message
		if ($Status != $::SUCCESS)
			{
			return (@Response);
			}

		@Response = ACTINIC::EncodeText($sExcludes);				# convert special characters to their hex equivalent
		($Status, $sExcludes) = @Response;
		if ($Status != $::SUCCESS)
			{
			return (@Response);
			}
		#
		# get the prompt label if one is specified
		# or the price prompt if none specified
		#
		my $sLabel;
		if (defined $sLabelPrompt)
			{
			$sLabel = $sLabelPrompt;
			}
		elsif (defined $nLabelPrompt)
			{
			$sLabel = ACTINIC::GetPhrase(-1, $nLabelPrompt);
			}
		else
			{
			$sLabel = ACTINIC::GetPhrase(-1, 66);
			}
		#
		# Now plug all the variables into a hash
		#
		my %hVariables;
		$hVariables{$::VARPREFIX . 'PRICEPROMPT'} = $sLabel;
		$hVariables{$::VARPREFIX . 'COST'} = $sPrice;
		$hVariables{$::VARPREFIX . 'TAXMESSAGE'} = $sExcludes;
		if(defined $sQuantityPrompt)
			{
			$hVariables{$::VARPREFIX . 'DISCOUNT_QUANTITY'} = $sQuantityPrompt;
			}
		else
			{
			$hVariables{$::VARPREFIX . 'DISCOUNT_QUANTITY'} = '';
			}
		#
		# Ditch the XML tags
		#
		$hVariables{'<Actinic:RETAIL_PRICE_TEXT>'} = '';
		$hVariables{'</Actinic:RETAIL_PRICE_TEXT>'} = '';		
		#
		# Now plug them into the template
		#
		$sLine = $$rPriceTemplate;
		@Response = ACTINIC::TemplateString($sLine, \%hVariables); # make the substitutions
		($Status, $Message, $sLine) = @Response;
		if ($Status != $::SUCCESS)
			{
			return (@Response);
			}
		}
	return ($::SUCCESS, "", $sLine, 0);
	}

#######################################################
#
# GetPriceTemplate - get the price template
#
# Params:	none
#
# Returns:	($ReturnCode, $Error, \$sPriceTemplate)
#				if $ReturnCode = $::FAILURE, the operation failed
#					for the reason specified in $Error
#				Otherwise everything is OK
#
#######################################################

sub GetPriceTemplate
	{
	#
	# If we have already read the template just return it
	#
	if($ActinicOrder::sPriceTemplate ne '')
		{
		return($::SUCCESS, '', \$ActinicOrder::sPriceTemplate);
		}
	#
	# Read the template from the file
	#
	my $sFilename = ACTINIC::GetPath()."price.html";
	unless (open (TFFILE, "<$sFilename"))
		{
		return($::FAILURE, ACTINIC::GetPhrase(-1, 21, $sFilename, $!), '', 0);
		}

	{
	local $/;
	$ActinicOrder::sPriceTemplate = <TFFILE>;								# read the entire file
	}
	close (TFFILE);
	return($::SUCCESS, '', \$ActinicOrder::sPriceTemplate);
	}

#######################################################
#
# FormatTaxMessage - format the tax exclusion message
#	into a text string
#
# Params:	$_[0] - the tax 1 band
#				$_[1] - the tax 2 band
#				$_[2] - the unit cost
#				$_[3] - the unit retail cost
#				$_[4] - tax 1 value if calculated separatley
#				$_[5] - tax 2 value if calculated separatley
#
# Returns:	($ReturnCode, $Error, $sFormattedText, 0)
#				if $ReturnCode = $::FAILURE, the operation failed
#					for the reason specified in $Error
#				Otherwise everything is OK
#
#######################################################

sub FormatTaxMessage
	{
	my ($nTax1Band, $nTax2Band, $nPrice, $nRetailPrice, $nTax1, $nTax2) = @_;
	my ($sTaxMessage, $sFormat, $nTax1BandID, $nTax2BandID, $sTax1, $sTax2, $nTaxRate, $nCustomTax1Rate, $nCustomTax2Rate);
	#
	# Tax calculated separately for components?
	#
	my $bSeparateTax = ((defined $nTax1 && $nTax1 > 0) || # separated tax calculation if tax1
							  (defined $nTax2 && $nTax2 > 0));	# or tax2 is passed in and greater than 0
	#
	# format the tax description and rate for each applicable tax
	#
	$sTaxMessage = "";
	my $sTaxDescriptionFormat = ACTINIC::GetPhrase(-1, 229);

	if (defined $$::g_pTaxSetupBlob{'TAX_1'})
		{
		($nTax1BandID, $nTaxRate, $nCustomTax1Rate) = split /=/, $nTax1Band;
		if ($nTax1BandID == $ActinicOrder::CUSTOM &&	# custom tax
			 $nRetailPrice > 0)								# and the retail price is more than 0
			{
			my $nRate = ($nCustomTax1Rate / $nRetailPrice) * 100 * 100;
			$nTaxRate = RoundTax($nRate,
					$$::g_pTaxSetupBlob{TAX_1}{'ROUND_RULE'});
			}
		if ($bSeparateTax)
			{
			$sTax1 = $$::g_pTaxSetupBlob{'TAX_1'}{'NAME'};
			}
		elsif ($nTaxRate > 0)							# the custom tax rate is > 0
			{
			$sTax1 = sprintf($sTaxDescriptionFormat,
				$$::g_pTaxSetupBlob{'TAX_1'}{'NAME'},
				ActinicOrder::FormatTaxRate($nTaxRate));	# tax 1 description
			}
		}
	else
		{
		$sTax1 = '';											# no tax 1
		}
	if (defined $$::g_pTaxSetupBlob{'TAX_2'})
		{
		($nTax2BandID, $nTaxRate, $nCustomTax2Rate) = split /=/, $nTax2Band;
		if ($nTax2BandID == $ActinicOrder::CUSTOM &&
			 $nRetailPrice > 0)
			{
			my $nRate = ($nCustomTax2Rate / $nRetailPrice) * 100 * 100;
			$nTaxRate = RoundTax($nRate,
					$$::g_pTaxSetupBlob{TAX_2}{'ROUND_RULE'});
			}
		if ($bSeparateTax)
			{
			$sTax2 = $$::g_pTaxSetupBlob{'TAX_2'}{'NAME'};
			}
		elsif ($nTaxRate > 0)							# the custom tax rate is > 0
			{
			$sTax2 = sprintf($sTaxDescriptionFormat,
				$$::g_pTaxSetupBlob{'TAX_2'}{'NAME'},
				ActinicOrder::FormatTaxRate($nTaxRate));		# tax 1 description
			}
		}
	else
		{
		$sTax2 = '';											# no tax 2
		}

	#
	# get the appropriate description prompt for tax-inclusive or exclusive prices
	#
	if ($::g_pSetupBlob->{TAX_INCLUSIVE_PRICES})
		{
		$sFormat = ACTINIC::GetPhrase(-1, 219);		# 'Including: %s'
		}
	else
		{
		$sFormat = ACTINIC::GetPhrase(-1, 67);			# 'Excluding: %s'
		}

	if ($sTax1 ne '' && IsTaxLevied($nTax1Band) &&
		(!IsTaxLevied($nTax2Band) || $sTax2 eq ''))			# only tax 1 levied
		{
		$sTaxMessage = sprintf($sFormat, $sTax1);
		}
	elsif (($sTax1 eq '' || !IsTaxLevied($nTax1Band)) &&
			$sTax2 ne '' && IsTaxLevied($nTax2Band))		# only tax 2 levied
		{
		$sTaxMessage = sprintf($sFormat, $sTax2);
		}
	elsif ($sTax1 ne '' && IsTaxLevied($nTax1Band) && $sTax2 ne '' && IsTaxLevied($nTax2Band))		# both taxes are levied
		{
		my ($sCombined);
		$sCombined = ACTINIC::GetPhrase(-1, 68, $sTax1, $sTax2);# '%s and %s'
		$sTaxMessage = sprintf($sFormat, $sCombined);
		}
	#
	# check to see if we are displaying tax-inclusive prices
	#
	my @Response;
	if($::g_pSetupBlob->{TAX_INCLUSIVE_PRICES} && $::g_pSetupBlob->{TAX_EXCLUSIVE_PRICES})
		{
		#
		# See if the tax is already calculated
		#
		if (!$bSeparateTax)
			{
			@Response = ActinicOrder::CalculateTax($nPrice, 1, $nTax1Band, $nTax2Band, $nRetailPrice);
			if($Response[0] != $::SUCCESS)
				{
				return (@Response);
				}
			$nTax1 = $Response[2];
			$nTax2 = $Response[3];
			}
		if($nTax1 + $nTax2 > 0)			# if there is no tax, only display tax exclusive price
			{
			$nPrice = $nPrice + $nTax1 + $nTax2;
			@Response = ActinicOrder::FormatPrice($nPrice, $::TRUE, $::g_pCatalogBlob);
			$sTaxMessage = $Response[2] . ' ' . $sTaxMessage;
			}
		}

	return ($::SUCCESS, "", $sTaxMessage, 0);
	}

#######################################################
#
# IsTaxLevied - returns whether a tax is levied
#
# Params:	$_[0] - tax band
#
# Returns:	($ReturnCode)
#				if $ReturnCode = $::TRUE, the tax is levied
#
#######################################################

sub IsTaxLevied
	{
	my($nTaxBand) = @_;
	if($nTaxBand == $ActinicOrder::CUSTOM || $nTaxBand == $ActinicOrder::PRORATA)
		{
		return($::TRUE);
		}
	return($nTaxBand > $ActinicOrder::CUSTOM ? $::TRUE : $::FALSE);
	}

#######################################################
#
# GetTaxModelOpaqueData - returns tax model opaque data
#
# Returns:	($nReturnCode, $sMessage, $sTaxOpaqueData)
#				if $ReturnCode = $::TRUE, the tax opaque data found
#
#######################################################

sub GetTaxModelOpaqueData
	{
	my @arrModel = split(/=/, $$::g_pTaxSetupBlob{MODEL_OPAQUEDATA});
	my $nModelID = $arrModel[0];
	my $nAddress = $arrModel[1];
	my $sModelOpaqueData = sprintf('%d=%d=', $nModelID, $nAddress);
	my ($nTax, $sTaxKey);

	foreach $nTax(1..2)
		{
		$sTaxKey = 'TAX_' . $nTax;
		if(defined $$::g_pTaxSetupBlob{$sTaxKey} &&					# is this tax defined
			$$::g_pTaxSetupBlob{$sTaxKey}{SHIP_TAX_OPAQUE_DATA})	# and there's some opaque data
			{
			#
			# Get shipping tax data for this tax
			#
			$sModelOpaqueData .= $$::g_pTaxSetupBlob{$sTaxKey}{SHIP_TAX_OPAQUE_DATA};
			}
		else
			{
			$sModelOpaqueData .= '0=0=0==';
			}
		}

	foreach $nTax(1..2)
		{
		$sTaxKey = 'TAX_' . $nTax;
		if(defined $$::g_pTaxSetupBlob{$sTaxKey} &&					# is this tax defined
			$$::g_pTaxSetupBlob{$sTaxKey}{HAND_TAX_OPAQUE_DATA})	# and there's some opaque data
			{
			#
			# Get handling tax data for this tax
			#
			$sModelOpaqueData .= $$::g_pTaxSetupBlob{$sTaxKey}{HAND_TAX_OPAQUE_DATA};
			}
		else
			{
			$sModelOpaqueData .= '0=0=0==';
			}
		}
	return($::SUCCESS, '', $sModelOpaqueData);
	}

#######################################################
#
# GetProductTaxBands - returns product tax bands
#
# Params:	$_[0] - ref to product info
#
# Returns:	($nReturnCode, $sMessage, $sTax1OpaqueData, $sTax2OpaqueData)
#				if $ReturnCode = $::TRUE, the tax opaque data found
#
#######################################################

sub GetProductTaxBands
	{
	my($pProductInfo) = @_;
	my($sTax1OpaqueData, $sTax2OpaqueData);
	$sTax1OpaqueData = '0=0=0=';
	$sTax2OpaqueData = '0=0=0=';
	#
	# if tax 1 is levied
	#
	if(defined $$::g_pTaxSetupBlob{TAX_1})
		{
		my $sTaxID = $$::g_pTaxSetupBlob{TAX_1}{ID};					# get tax id
		if(defined $$pProductInfo{'TAX_' . $sTaxID})					# is it defined in the product info?
			{
			$sTax1OpaqueData = $$pProductInfo{'TAX_' . $sTaxID};	# set opaque data
			}
		else
			{
			return($::FALSE, 'Tax not found', $sTax1OpaqueData, $sTax2OpaqueData);
			}
		}
	#
	# if tax 2 is levied
	#
	if(defined $$::g_pTaxSetupBlob{TAX_2})
		{
		my $sTaxID = $$::g_pTaxSetupBlob{TAX_2}{ID};					# get tax id
		if(defined $$pProductInfo{'TAX_' . $sTaxID})					# is it defined in the product info?
			{
			$sTax2OpaqueData = $$pProductInfo{'TAX_' . $sTaxID};	# set opaque data
			}
		else
			{
			return($::FALSE, 'Tax not found', $sTax1OpaqueData, $sTax2OpaqueData);
			}
		}
	return($::TRUE, '', $sTax1OpaqueData, $sTax2OpaqueData);
	}

#######################################################
#
# GetShippingTaxBands - returns shipping tax bands
#
# Params:	$_[0] - ref to product info
#
# Returns:	($nReturnCode, $sMessage$sTax1OpaqueData, $sTax2OpaqueData)
#				if $ReturnCode = $::TRUE, the tax opaque data found
#
#######################################################

sub GetShippingTaxBands
	{
	my($sTax1OpaqueData, $sTax2OpaqueData);
	$sTax1OpaqueData = '0=0=0=';
	$sTax2OpaqueData = '0=0=0=';
	#
	# if tax 1 is levied
	#
	if(defined $$::g_pTaxSetupBlob{TAX_1})
		{
		$sTax1OpaqueData = $$::g_pTaxSetupBlob{TAX_1}{SHIP_TAX_OPAQUE_DATA};	# set opaque data
		}
	#
	# if tax 2 is levied
	#
	if(defined $$::g_pTaxSetupBlob{TAX_2})
		{
		$sTax2OpaqueData = $$::g_pTaxSetupBlob{TAX_2}{SHIP_TAX_OPAQUE_DATA};	# set opaque data
		}
	return($::TRUE, '', $sTax1OpaqueData, $sTax2OpaqueData);
	}

#######################################################
#
# GetHandlingTaxBands - returns Handling tax bands
#
# Params:	$_[0] - ref to product info
#
# Returns:	($nReturnCode, $sMessage$sTax1OpaqueData, $sTax2OpaqueData)
#				if $ReturnCode = $::TRUE, the tax opaque data found
#
#######################################################

sub GetHandlingTaxBands
	{
	my($sTax1OpaqueData, $sTax2OpaqueData);
	$sTax1OpaqueData = '0=0=0=';
	$sTax2OpaqueData = '0=0=0=';
	#
	# if tax 1 is levied
	#
	if(defined $$::g_pTaxSetupBlob{TAX_1})
		{
		$sTax1OpaqueData = $$::g_pTaxSetupBlob{TAX_1}{HAND_TAX_OPAQUE_DATA};	# set opaque data
		}
	#
	# if tax 2 is levied
	#
	if(defined $$::g_pTaxSetupBlob{TAX_2})
		{
		$sTax2OpaqueData = $$::g_pTaxSetupBlob{TAX_2}{HAND_TAX_OPAQUE_DATA};	# set opaque data
		}
	return($::TRUE, '', $sTax1OpaqueData, $sTax2OpaqueData);
	}

#######################################################
#
# FormatSchedulePrices - format the tax exclusion message
#	into a text string
#
# Params:	$_[0] - ref to the product hash
#				$_[1] - the price schedule
#				$_[2] - the variant list
#				$_[3] - the price label
#				$_[4] - the number of prices to show
#
# Returns:	($ReturnCode, $Error, $sFormattedText, 0)
#				if $ReturnCode = $::FAILURE, the operation failed
#					for the reason specified in $Error
#				Otherwise everything is OK
#
#######################################################

sub FormatSchedulePrices
	{
	if (!defined $_[1])
		{
		return ($::FAILURE, ACTINIC::GetPhrase(-1, 12, 'FormatSchedulePrices'), 0, 0);
		}
	my ($pProduct, $nScheduleID, $rVariantList, $sLabel, $nSinglePrice, $bIgnoreComponentPrices) = @_;
	my (@Response, $Status, $sLine, %PriceBreak, $pComponent);
	my $ComponentPrice = 0;
	my $ComponentRetailPrice = 0;
	my %Component;
	my ($nAlreadyTaxed, $nAlreadyTaxedRetail, $nTax1, $nTax2, $nTaxBase);
	#
	# Component prices are not ignored by default
	#
	if (!defined $bIgnoreComponentPrices)
		{
		$bIgnoreComponentPrices = $::FALSE;
		}
	#
	# Determine Tax bands
	#
	@Response = GetProductTaxBands($pProduct);
	if ($Response[0] != $::SUCCESS)
		{
		return (@Response);
		}
	my $nTax1Band = $Response[2];
	my $nTax2Band = $Response[3];

	foreach $pComponent (@{$pProduct->{COMPONENTS}})
		{
		@Response = ActinicOrder::FindComponent($pComponent, $$rVariantList);
		($Status, %Component) = @Response;
		if ($Status != $::SUCCESS)
			{
			return ($Status,$Component{text});
			}
		if( $Component{quantity} > 0 )
			{
			@Response = ActinicOrder::GetComponentPrice($Component{price}, $$pProduct{"MIN_QUANTITY_ORDERABLE"}, $Component{quantity}, $nScheduleID);
			if ($Response[0] != $::SUCCESS)
				{
				return (@Response);
				}
			my $nItemPrice = $Response[2];	# store component price
			if (!$nItemPrice)
				{
				$nItemPrice = $Component{'RetailPrice'}
				}
			#
			# Calculate retail price of one product
			#
			@Response = GetComponentPrice($Component{price}, 1, $Component{quantity}, $ActinicOrder::RETAILID);
			if ($Response[0] != $::SUCCESS)
				{
				return (@Response);
				}
			my $nRetailPrice = $Response[2];
			if (!$nRetailPrice)
				{
				$nRetailPrice = $Component{'RetailPrice'}
				}
			$ComponentPrice += $nItemPrice;	# summarize component prices
			$ComponentRetailPrice += $nRetailPrice;
			#
			# See if custom tax handling required
			#
			if ($Component{AssociatedTax})
				{
				$nAlreadyTaxed += $nItemPrice;
				$nAlreadyTaxedRetail += $nRetailPrice;
				#
				# If the associated product price is available for this component
				# then its retail price should be passed in to CalculateTax to
				# have correct custom tax
				#
				if (defined $Component{'AssociatedPrice'})
					{
					@Response = GetComponentPrice($Component{'AssociatedPrice'}, $$pProduct{"MIN_QUANTITY_ORDERABLE"}, $Component{quantity}, $ActinicOrder::RETAILID);
					if ($Response[0] != $::SUCCESS)
						{
						return (@Response);
						}
					$nRetailPrice = $Response[2];
					}
				#
				# Calculate the taxes on this component
				#
				@Response = GetProductTaxBands(\%Component);
				if ($Response[0] != $::SUCCESS)
					{
					return (@Response);
					}
				@Response = CalculateTax($nItemPrice, 1, $Response[2], $Response[3], $nRetailPrice);
				if ($Response[0] != $::SUCCESS)
					{
					return (@Response);
					}
				$nTax1 += $Response[2];					# calculate the tax 1 composite total
				$nTax2 += $Response[3];					# calculate the tax 2 composite total
				}
			elsif ($pComponent->[$::CBIDX_SEPARATELINE] &&	# the component is separated
					 !$$pProduct{NO_ORDERLINE})					# and the main product has order line
				{
				my ($nBandID, $nPercent, $nFlatRate, $sBandName) = split /=/, $nTax1Band;
				my $nCompTax1Band = $nTax1Band;
				my $nCompTax2Band = $nTax2Band;
				if ($nBandID == $ActinicOrder::CUSTOM)
					{
					$nCompTax1Band = $ActinicOrder::EXEMPT;
					}
				($nBandID, $nPercent, $nFlatRate, $sBandName) = split /=/, $nTax2Band;
				if ($nBandID == $ActinicOrder::CUSTOM)
					{
					$nCompTax1Band = $ActinicOrder::EXEMPT;
					}
				@Response = CalculateTax($nItemPrice, 1, $nCompTax1Band, $nCompTax2Band, $nRetailPrice);
				if ($Response[0] != $::SUCCESS)
					{
					return (@Response);
					}
				$nTax1 += $Response[2];					# calculate the tax 1 composite total
				$nTax2 += $Response[3];					# calculate the tax 2 composite total
				$nAlreadyTaxed += $nItemPrice;
				$nAlreadyTaxedRetail += $nRetailPrice;
				}
			#
			# Determine price breaks
			#
			my $nSchedulePrice;
			if( ref($Component{price}) eq 'HASH' )					# If it is just a simple price,
				{
				my $raPrices = \$Component{price}->{$nScheduleID};
				foreach $nSchedulePrice (@$$raPrices)		# check each item in the price hash
					{
					$PriceBreak{int($$nSchedulePrice[0] / $Component{quantity} + 0.499999)} = defined $$nSchedulePrice[2] ?
						int($$nSchedulePrice[2] / $Component{quantity}) : -1;	# and note qty breaks (hash used to be sure that the entries are unique)
					}
				}
			}
		}
	#
	# add the product price to the html
	#
	$sLine = '';
	my ($sPrice, $raPrices, $raTemp, $nSchedulePrice, $nSchedulePriceCount);
	if (defined $$pProduct{PRICES})
		{
		$raPrices = \$$pProduct{PRICES}->{$nScheduleID};
		foreach $nSchedulePrice (@$$raPrices)		# check each item in the price hash
			{
			$PriceBreak{$$nSchedulePrice[0]} = defined $$nSchedulePrice[2] ? $$nSchedulePrice[2] : -1;	# and note qty breaks (hash used to be sure that the entries are unique)
			}
		}
	$nSchedulePriceCount = keys %PriceBreak;

	my $nRetailPrice = $$pProduct{PRICE};
	if ((!$nSinglePrice) && $nSchedulePriceCount > 1)
		{
		my ($nSchedulePrice, $index);
		#
		# Create price break array
		#
		my $nIndex = 0;
		my $nLimit;
		my $nLastPrice;

		foreach $nLimit (sort {$a <=> $b} (keys %PriceBreak))
			{
			#
			# Insert ranges
			#
			$$raTemp->[$nIndex]->[0] = $nLimit;
			$$raTemp->[$nIndex]->[2] = $PriceBreak{$nLimit};
			#
			# Insert price
			#
			my $nPrice;
			my $MaxFound = -1;
			#
			# Find product price first
			#
			foreach (@$$raPrices)									# Find best fit for this quantity
				{
				if( $_->[0] > $MaxFound and $nLimit >= $_->[0] )
					{
					$MaxFound = $_->[0];
					$nPrice   = $_->[1];
					}
				}
			#
			# Determine component price
			#
			my $nPriceModel = $$pProduct{PRICING_MODEL};
			my ($nAlreadyTaxed, $nTax1, $nTax2, $nTaxBase);

			if( $nPriceModel != $ActinicOrder::PRICING_MODEL_STANDARD )
				{
				$ComponentPrice = 0;
				foreach $pComponent (@{$pProduct->{COMPONENTS}})
					{
					@Response = ActinicOrder::FindComponent($pComponent, $$rVariantList);
					($Status, %Component) = @Response;
					if ($Status != $::SUCCESS)
						{
						return ($Status,$Component{text});
						}
					if( $Component{quantity} > 0 )
						{
						@Response = ActinicOrder::GetComponentPrice($Component{price}, $nLimit, $Component{quantity}, $nScheduleID);
						if ($Response[0] != $::SUCCESS)
							{
							return (@Response);
							}
						my $nItemPrice = $Response[2];	# store component price
						#
						# Calculate retail price
						#
						@Response = GetComponentPrice($Component{price}, 1, $Component{quantity}, $ActinicOrder::RETAILID);
						if ($Response[0] != $::SUCCESS)
							{
							return (@Response);
							}
						my $nRetailPrice = $Response[2];
						$ComponentPrice += $nItemPrice;	# summarize component prices
						#
						# See if custom tax handling required
						#
						if ($Component{AssociatedTax})
							{
							#
							# If the associated product price is available for this component
							# then its retail price should be passed in to CalculateTax to
							# have correct custom tax
							#
							if (defined $Component{'AssociatedPrice'})
								{
								@Response = GetComponentPrice($Component{'AssociatedPrice'}, $$pProduct{"MIN_QUANTITY_ORDERABLE"}, $Component{quantity}, $ActinicOrder::RETAILID);
								if ($Response[0] != $::SUCCESS)
									{
									return (@Response);
									}
								$nRetailPrice = $Response[2];
								}
							#
							# Calculate the taxes on this component
							#
							@Response = GetProductTaxBands(\%Component);
							if ($Response[0] != $::SUCCESS)
								{
								return (@Response);
								}
							@Response = CalculateTax($nItemPrice, $Component{quantity}, $Response[2], $Response[3], $nRetailPrice);
							if ($Response[0] != $::SUCCESS)
								{
								return (@Response);
								}
							$nTax1 += $Response[2];			# calculate the tax 1 composite total
							$nTax2 += $Response[3];			# calculate the tax 2 composite total
							$nAlreadyTaxed += $nItemPrice;
							}
						elsif ($pComponent->[$::CBIDX_SEPARATELINE] &&	# the component is separated
								 !$$pProduct{NO_ORDERLINE})					# and the main product has order line
							{
							my ($nBandID, $nPercent, $nFlatRate, $sBandName) = split /=/, $nTax1Band;
							my $nCompTax1Band = $nTax1Band;
							my $nCompTax2Band = $nTax2Band;
							if ($nBandID == $ActinicOrder::CUSTOM)
								{
								$nCompTax1Band = $ActinicOrder::EXEMPT;
								}
							($nBandID, $nPercent, $nFlatRate, $sBandName) = split /=/, $nTax2Band;
							if ($nBandID == $ActinicOrder::CUSTOM)
								{
								$nCompTax1Band = $ActinicOrder::EXEMPT;
								}
							@Response = CalculateTax($nItemPrice, $Component{quantity}, $nCompTax1Band, $nCompTax2Band, $nRetailPrice);
							if ($Response[0] != $::SUCCESS)
								{
								return (@Response);
								}
							$nTax1 += $Response[2];					# calculate the tax 1 composite total
							$nTax2 += $Response[3];					# calculate the tax 2 composite total
							$nAlreadyTaxed += $nItemPrice;
							}
						}
					}
				if( $nPriceModel == $ActinicOrder::PRICING_MODEL_PROD_COMP )
					{
					$nPrice += $ComponentPrice;
					$nTaxBase = $nPrice - $nAlreadyTaxed;
					}
				elsif( $nPriceModel == $ActinicOrder::PRICING_MODEL_COMP &&
						!$bIgnoreComponentPrices)
					{
					$nPrice = $ComponentPrice;
					$nTaxBase = $nPrice - $nAlreadyTaxed;
					}
				}
			#
			# Store price data
			#
			$$raTemp->[$nIndex]->[1] = $nPrice;
			#
			# If we have components taxed separately
			#
			if ($nAlreadyTaxed > 0)
				{
				@Response = ActinicOrder::CalculateTax($nTaxBase, 1, $nTax1Band, $nTax2Band, $nTaxBase);
				if($Response[0] != $::SUCCESS)
					{
					return (@Response);
					}
				$$raTemp->[$nIndex]->[3] = $nTax1 + $Response[2];			# calculate the tax 1 composite total
				$$raTemp->[$nIndex]->[4] = $nTax2 + $Response[3];			# calculate the tax 2 composite total
				}
			#
			# Sort out duplicated prices
			#
			if ($nPrice == $nLastPrice)
				{
				pop @$$raTemp;
				}
			else
				{
				$nIndex++;
				$nLastPrice = $nPrice;
				}
			}
		$raPrices = $raTemp;
		#
		# Get labels for each price break
		#
		@Response = GetQuantityLabels($raPrices);
		my $rarrQtyLabels = $Response[2];
		#
		# Now build the HTML for result
		#
		$nIndex = 0;
		my $sPriceLines = "";
		foreach (@$$raPrices)
			{
			@Response = ActinicOrder::FormatCompletePrice($_->[1],
				$nTax1Band, $nTax1Band, $nRetailPrice, undef, "", $$rarrQtyLabels[$nIndex++], $_->[3], $_->[4]);
			if ($Response[0] != $::SUCCESS)
				{
				return (@Response);
				}
			$sPriceLines .=  $Response[2];
			}
		$sLine = ACTINIC::GetPhrase(-1, 2203, $sLabel, $sPriceLines);
		}
	else
		{
		if(defined $$raPrices->[0])
			{
			$sPrice = $$raPrices->[0]->[1];
			}
		else
			{
			$sPrice = $$pProduct{PRICE};
			}

		my $nRetailPrice = $$pProduct{PRICE};
		my $nPriceModel = $$pProduct{PRICING_MODEL};
		my $nRetailTaxBase = $nRetailPrice;
		if( $nPriceModel == $ActinicOrder::PRICING_MODEL_PROD_COMP )
			{
			$sPrice += $ComponentPrice;
			$nTaxBase = $sPrice - $nAlreadyTaxed;
			$nRetailTaxBase = $nRetailPrice + $ComponentRetailPrice - $nAlreadyTaxedRetail;
			}
		elsif( $nPriceModel == $ActinicOrder::PRICING_MODEL_COMP &&
				!$bIgnoreComponentPrices)
			{
			$sPrice = $ComponentPrice;
			$nTaxBase = $sPrice - $nAlreadyTaxed;
			$nRetailTaxBase = $nRetailPrice + $ComponentRetailPrice - $nAlreadyTaxedRetail;
			}
		#
		# If we have components taxed separately
		#
		if ($nAlreadyTaxed > 0 &&
			 $nTaxBase > 0)
			{
			@Response = ActinicOrder::CalculateTax($nTaxBase, 1, $nTax1Band, $nTax2Band, $nRetailTaxBase);
			if($Response[0] != $::SUCCESS)
				{
				return (@Response);
				}
			$nTax1 += $Response[2];			# calculate the tax 1 composite total
			$nTax2 += $Response[3];			# calculate the tax 2 composite total
			}

		@Response = ActinicOrder::FormatCompletePrice($sPrice, $nTax1Band, $nTax2Band, $nRetailPrice, undef, $sLabel, "", $nTax1, $nTax2);

		if ($Response[0] != $::SUCCESS)
			{
			return (@Response);
			}
		$sLine .= "$Response[2]<BR>";
		}

	return($::SUCCESS, '', $sLine);
	}

#######################################################
#
# GetQuantityLabels - Get the quantity labels for a schedule
#
# Params:	$_[0] - ref to the price array
#
# Returns:	($ReturnCode, $Error, @QuantityLabels)
#				if $ReturnCode = $::FAILURE, the operation failed
#					for the reason specified in $Error
#				Otherwise everything is OK
#
#######################################################

sub GetQuantityLabels
	{
	my ($raPrices) = @_;
	my ($nSchedulePriceCount, $nLastQty, $sQtyLabel, $nArrIndex, $nPromptID, @arrQtyLabels);

	if (defined $$raPrices)
		{
		$nSchedulePriceCount = @$$raPrices;
		}
	else
		{
		push @arrQtyLabels, "";
		return ($::SUCCESS, '', \@arrQtyLabels);
		}

	$nLastQty = -1;
	for($nArrIndex = $nSchedulePriceCount - 1; $nArrIndex >= 0; $nArrIndex--)
		{
		if($nArrIndex == $nSchedulePriceCount - 1)	# highest quantity?
			{
			if(defined $$raPrices->[$nArrIndex]->[2] &&	# maximum defined?
				$$raPrices->[$nArrIndex]->[2] > 0)
				{
				if($$raPrices->[$nArrIndex]->[0] == $$raPrices->[$nArrIndex]->[2])	# max == min?
					{
					#
					# check for plurality
					#
					$nPromptID = $$raPrices->[$nArrIndex]->[0] > 1 ?
						286 : 287;
					#
					# format the quantity
					#
					$arrQtyLabels[$nArrIndex] =
						sprintf(ACTINIC::GetPhrase(-1, $nPromptID), $$raPrices->[$nArrIndex]->[0]);
					}
				else
					{
					#
					# format as a range
					#
					$arrQtyLabels[$nArrIndex] =
						sprintf(ACTINIC::GetPhrase(-1, 289),
							$$raPrices->[$nArrIndex]->[0],
							$$raPrices->[$nArrIndex]->[2]);
					}
				}
			else												# non max
				{
				#
				# format as 'n or more'
				#
				$arrQtyLabels[$nArrIndex] =
					sprintf(ACTINIC::GetPhrase(-1, 288), $$raPrices->[$nArrIndex]->[0],
						$$raPrices->[$nArrIndex]->[2]);
				}
			}
		elsif($$raPrices->[$nArrIndex]->[0] == $nLastQty)	# max == min?
			{
			#
			# check for plurality
			#
			$nPromptID = $$raPrices->[$nArrIndex]->[0] > 1 ?
				286 : 287;
			#
			# format the quantity
			#
			$arrQtyLabels[$nArrIndex] =
				sprintf(ACTINIC::GetPhrase(-1, $nPromptID), $$raPrices->[$nArrIndex]->[0]);
			}
		elsif ($nArrIndex == 0)							# lowest quantity?
			{
			if($nLastQty > 1)
				{
				if ($$raPrices->[$nArrIndex]->[0] < 2)
					{
					#
					# format as 'n or fewer'
					#
					$arrQtyLabels[$nArrIndex] = sprintf(ACTINIC::GetPhrase(-1, 290), $nLastQty);
					}
				else
					{
					#
					# format as a range
					#
					$arrQtyLabels[$nArrIndex] = sprintf(ACTINIC::GetPhrase(-1, 289),
						$$raPrices->[$nArrIndex]->[0], $nLastQty);
					}
				}
			else
				{
				$arrQtyLabels[$nArrIndex] =
					sprintf(ACTINIC::GetPhrase(-1, 287), $nLastQty);
				}
			}
		else
			{
			#
			# format as range
			#
			$arrQtyLabels[$nArrIndex] =
				sprintf(ACTINIC::GetPhrase(-1, 289), $$raPrices->[$nArrIndex]->[0], $nLastQty);
			}
		#
		# save last quantity
		#
		$nLastQty = $$raPrices->[$nArrIndex]->[0] - 1;
		}
	return ($::SUCCESS, '', \@arrQtyLabels);
	}

#######################################################
#
# ReadPrice - read the price from the string
#
# Params:	0 - the price packed in a string
#				1 - a reference to the currency table
#
# Returns:	0 - status
#				1 - error message
#				2 - price (in catalog format)
#
#######################################################
#   WARNING WARNING WARNING WARNING WARNING WARNING
#######################################################
#
# Any modification of this function's interface or
# basic functionality have to be reflected in the
# shipping plug in!!!
# 20 Feb 2002 gmenyhert
#
#######################################################
#   WARNING WARNING WARNING WARNING WARNING WARNING
#######################################################

sub ReadPrice
	{
	if ($#_ != 1)
		{
		return ($::FAILURE, ACTINIC::GetPhrase(-1, 12, 'ReadPrice'), 0, 0);
		}

	my ($sPrice, $pCurrencyTable);
	($sPrice, $pCurrencyTable) = @_;

	my ($nNumDigits, $nGrouping, $sDecimal, $sThousand, $eNegOrder, $ePosOrder, $sCurSymbol);
	$nNumDigits = $$pCurrencyTable{"ICURRDIGITS"};	# read the currency format values from the catalog blob
	$nGrouping = $$pCurrencyTable{"IMONGROUPING"};
	$sDecimal = $$pCurrencyTable{"SMONDECIMALSEP"};
	$sThousand = $$pCurrencyTable{"SMONTHOUSANDSEP"};
	$eNegOrder = $$pCurrencyTable{"INEGCURR"};
	$ePosOrder = $$pCurrencyTable{"ICURRENCY"};
	$sCurSymbol = $$pCurrencyTable{"SCURRENCY"};
	#
	# check if the string represents a negative value (contains a "-" or "()")
	#
	my ($bNegative);
	$bNegative = ($sPrice =~ /-/ || $sPrice =~ /\(/ );
	#
	# replace the decimal symbol with a generic "."
	# To be safe, first strip the currency symbol (if any), then strip the thousand separator (if any)
	#
	$sPrice =~ s/&#[0-9]{1,4};//g;						# strip special currency symbols of format &#nnnn;
	$sCurSymbol =~ s/([.\$\\])/\\$1/g;					# escape shady characters										
	$sPrice =~ s/$sCurSymbol//;
	
	if ($sDecimal ne $sThousand)
		{
		$sThousand =~ s/([.\$\\])/\\$1/g;
		$sPrice =~ s/$sThousand//g;
		}
	$sDecimal =~ s/([.\$\\])/\\$1/g;
	if ($nNumDigits != 0)
		{
		$sPrice =~ s/$sDecimal/./g;
		}
	#
	# now strip all non digit values from the string.  The exception is the decimal place
	# which is replaced with the standard "."
	#
	$sPrice =~ s/[^.0-9]//g;
	#
	# now adjust the price for catalog format
	#
	$sPrice *= 10 ** $nNumDigits;
	#
	# negate the value if necessary
	#
	if ($bNegative)
		{
		$sPrice *= -1.0;
		}
	#
	# guarantee that the price is an integer - sometimes rounding comes
	# in so that 2.55 * 100 != 255 - but is something like 254.9999
	#
	$sPrice = int ($sPrice + 0.5);

	return ($::SUCCESS, '', $sPrice, 0);
	}

	
############################################################
#
#  sub CheckBuyerLimit
#
#  Check shopping cart against customer spending limit
#
#  Parameters: 0 - Shopping cart ID
#              1 - Destination URL for bouncing page (optional)
#					2 - Frame clear flag (if true the target will be set to top)
#
#  Returns:	0 - status (success - OK, failure - exceeded limit)
#  				1 - HTML to be printed if STATUS is not SUCCESS
#
#  Ryszard Zybert  Jun  2 20:40:47 BST 2000
#
#  Copyright (c) Actinic Software Ltd (2000)
#
############################################################

sub CheckBuyerLimit
	{
	my ($sCartId, $sDestinationURL, $bClearFrame) = @_;
	my $sDigest = $ACTINIC::B2B->Get('UserDigest');			# Get User ID once
	my $nLowerBound = $$::g_pSetupBlob{'MIN_ORDER_VALUE'};
	my $nUpperBound = $$::g_pSetupBlob{'MAX_ORDER_VALUE'};
	my $nBuyerLimit = 0;
	#
	# See what is the limit based on the buyer type (registered/retail)
	#	
	if( $sDigest )
		{
		my ($Status, $Message, $pBuyer) = ACTINIC::GetBuyer($sDigest, ACTINIC::GetPath()); # look up the buyer
		if ($Status != $::SUCCESS)						# error out
			{
			return ($Status, $Message, "");
			}
		#
		# Set the limit for the buyer
		#
		if ($pBuyer->{LimitOrderValue})
			{
			$nBuyerLimit = $pBuyer->{MaximumOrderValue};
			}
		}
	#
	# Now see what is the current order value
	#
	my ($pCartList, @EmptyArray);
	my @Response = $::Session->GetCartObject();
	if ($Response[0] != $::SUCCESS)					# general error
		{
		return (@Response);								# error so return empty string
		}
	my $pCartObject = $Response[2];

	@Response = $pCartObject->SummarizeOrder($::TRUE);# calculate the order total
	if ($Response[0] != $::SUCCESS)
		{
		return (@Response);
		}
	my ($Ignore0, $Ignore1, $nSubTotal, $nShipping, $nTax1, $nTax2, $nTotal, $nShippingTax1, $nShippingTax2, $nHandling, $nHandlingTax1, $nHandlingTax2) = @Response;
	#
	# Now see the if the limit is exceeded
	#
	my $sLimit;
	my $nPromptID;
	if ($nUpperBound > 0 &&
		 $nTotal > $nUpperBound)
		{
		@Response = ActinicOrder::FormatPrice($nUpperBound, $::TRUE, $::g_pCatalogBlob);	# Format buyer maximum
		if ($Response[0] != $::SUCCESS)
			{
			return (@Response);
			}
		$sLimit = $Response[2];
		$nPromptID = 2349;
		}
	elsif ($nBuyerLimit > 0 &&
	 		 $nBuyerLimit < $nTotal)
	 	{
		@Response = ActinicOrder::FormatPrice($nBuyerLimit, $::TRUE, $::g_pCatalogBlob);	# Format buyer maximum
		if ($Response[0] != $::SUCCESS)
			{
			return (@Response);
			}
		$sLimit = $Response[2];
		$nPromptID = 299;	 	
	 	}
	elsif ($nLowerBound > 0 &&
			 $nTotal < $nLowerBound)
		{
		@Response = ActinicOrder::FormatPrice($nLowerBound, $::TRUE, $::g_pCatalogBlob);	# Format buyer minimum
		if ($Response[0] != $::SUCCESS)
			{
			return (@Response);
			}
		$sLimit = $Response[2];				
		$nPromptID = 2348;
		}
	#
	# Format the message and bounce away if the order total is not fine
	#
	if ($sLimit ne "")
		{
		my ($sLocalPage, $sHTML);
		if( !$sDestinationURL )							# if destination page is not given, we get the last page visited
			{
			$sDestinationURL = $::Session->GetLastShopPage();
			}

		if ($$::g_pSetupBlob{UNFRAMED_CHECKOUT} && # if the checkout is unframed,
			 $$::g_pSetupBlob{UNFRAMED_CHECKOUT_URL})
			{
			$sDestinationURL = $$::g_pSetupBlob{UNFRAMED_CHECKOUT_URL};	# return to the given URL
			}

		@Response = ActinicOrder::FormatPrice($nTotal, $::TRUE, $::g_pCatalogBlob);	# Format current total
		if ($Response[0] != $::SUCCESS)
			{
			return (@Response);
			}
		my $sTotal = $Response[2];

		my $nDelay = 5;									# Show warning for 5 seconds
		if( !$bClearFrame )
			{
			$nDelay = 2;									# The warning shows up twice, so - cut it to 2 seconds each
			}
		#
		# Take care on frames when bouncing
		#
		my $bClear = ($bClearFrame && ACTINIC::IsCatalogFramed());
		if ($$::g_pSetupBlob{UNFRAMED_CHECKOUT})
			{
			$bClear = (!$bClearFrame && ACTINIC::IsCatalogFramed());
			}
		else
			{
			$bClear = ($bClearFrame && ACTINIC::IsCatalogFramed() && $$::g_pSetupBlob{UNFRAMED_CHECKOUT});
			}

		@Response = ACTINIC::BounceToPageEnhanced($nDelay, ACTINIC::GetPhrase(-1, 1962) . ACTINIC::GetPhrase(-1, $nPromptID, $sTotal, $sLimit) . ACTINIC::GetPhrase(-1, 1970) . ACTINIC::GetPhrase(-1, 2054),
																$$::g_pSetupBlob{CHECKOUT_DESCRIPTION},
																$::g_sWebSiteUrl,
																$::g_sContentUrl, $::g_pSetupBlob, $sDestinationURL, \%::g_InputHash,
																$bClear); 			# bounce back in the broswer
		my ($Status, $Message, $sHTML) = @Response;	# parse the response
		if ($Status != $::SUCCESS)						# error out
			{
			return (@Response);
			}
		return ($::BADDATA, $sHTML);				# return the goods
		}

	return ($::SUCCESS,'');		# OK - either Buyer within the limit or anonymous buyer
	}

##############################################################################################################
#
# Financial Processing - End
#
##############################################################################################################

#######################################################
#
# GetBuyerLocationSelections - Get the selection HTML
#								for a list of buyer addresses
#
# Author:	Mike Purnell
#
# Input:	$plistValidAddresses - list of valid addresses
#			$sCountryComboName	- the name of the country combo
#			$sStateComboName		- name of the state combo
#			$sCountryComboID		- the ID of the country combo
#			$sStateComboID			- ID of the state combo
#			$sPrefix					- INVOICE or DELIVERY
#			$nDefaultID				- the ID of the default address
#
#
# Returns:	0 - status
#				1 - error message
#				2 - the selection HTML for the country
#				3 - the selection HTML for the state
#
#######################################################

sub GetBuyerLocationSelections
	{
	my ($plistValidAddresses, $sCountryComboName, $sStateComboName, $sCountryComboID, $sStateComboID, $sPrefix, $nDefaultID) = @_;
	my ($pAddress, $sCountrySelectHTML, $sStateSelectHTML, $nPhraseID);
	my %hashRegionCode;
	#
	# Go through the list of valid addresses and map unique countries and states
	# Also pick up any previous selection
	#
	foreach $pAddress (@$plistValidAddresses)
		{
		#
		# Have we seen this country before?
		#
		if(!defined $hashRegionCode{$pAddress->{CountryCode}})
			{
			#
			# Add to hash
			#
			$hashRegionCode{$pAddress->{CountryCode}} = 1;
			#
			# Is this our current country selection
			#
			if ($pAddress->{CountryCode} eq $::g_LocationInfo{$sPrefix. '_COUNTRY_CODE'} ||
				((!defined $::g_LocationInfo{$sPrefix. '_COUNTRY_CODE'} ||
				$::g_LocationInfo{$sPrefix. '_COUNTRY_CODE'} eq "") &&
				$pAddress->{ID} == $nDefaultID))
				{
				$nPhraseID = 1219;						# use SELECTED OPTION
				}
			else
				{
				$nPhraseID = 1220;						# use OPTION
				}
			#
			# Add the OPTION to the country list
			#
			$sCountrySelectHTML .= sprintf(ACTINIC::GetPhrase(-1, $nPhraseID),
				$pAddress->{CountryCode}, ACTINIC::GetCountryName($pAddress->{CountryCode}));
			}
		#
		# If the state is present, add to the list (if it's unique)
		#
		if($pAddress->{StateCode} ne '')
			{
			#
			# If a new state
			#
			if(!defined $hashRegionCode{$pAddress->{StateCode}})
				{
				#
				# Add to hash
				#
				$hashRegionCode{$pAddress->{StateCode}} = 1;
				if ($pAddress->{StateCode} eq $::g_LocationInfo{$sPrefix. '_REGION_CODE'} ||
				((!defined $::g_LocationInfo{$sPrefix. '_REGION_CODE'} ||
				$::g_LocationInfo{$sPrefix. '_REGION_CODE'} eq "")&&
				$pAddress->{ID} == $nDefaultID))
					{
					$nPhraseID = 1219;					# use SELECTED OPTION
					}
				else
					{
					$nPhraseID = 1220;					# use OPTION
					}
				#
				# Add the OPTION to the state list
				#
				$sStateSelectHTML .= sprintf(ACTINIC::GetPhrase(-1, $nPhraseID),
					$pAddress->{StateCode}, ACTINIC::GetCountryName($pAddress->{StateCode}));
				}
			}
		}
	#
	# If we have some country options
	#
	if($sCountrySelectHTML ne '')
		{
		#
		# Add the SELECT tags
		#
		$sCountrySelectHTML = sprintf(ACTINIC::GetPhrase(-1, 1204),
			$sCountryComboID, $sCountryComboName) .
			sprintf(ACTINIC::GetPhrase(-1, 1220), $ActinicOrder::UNDEFINED_REGION, ACTINIC::GetPhrase(-1, 193)) .
			$sCountrySelectHTML . ACTINIC::GetPhrase(-1, 1205);
		}
	#
	# If we have some state options
	#
	if($sStateSelectHTML ne '')
		{
		#
		# Add the SELECT tags
		#
		$sStateSelectHTML = sprintf(ACTINIC::GetPhrase(-1, 1204),
			$sStateComboID, $sStateComboName) .
			sprintf(ACTINIC::GetPhrase(-1, 1220), $ActinicOrder::UNDEFINED_REGION, ACTINIC::GetPhrase(-1, 194)) .
			$sStateSelectHTML . ACTINIC::GetPhrase(-1, 1205);
		}
	return($::SUCCESS, '', $sCountrySelectHTML, $sStateSelectHTML);
	}

#######################################################
#
# ValidateOrderDetails - Validate the order details.
#	If they are valid, return ::SUCCESS.  If any are
#	invalid, return ::BADDATA with a modified OrderDetails
#	page packed into $Error.  Can also return ::FAILURE
#	on unrecoverable error.
#
# Params:	0 - orderline details
#				1 - (optional) index of item in cart
#				 	 that is being edited
#					-1 => Adding quantity (Default)
#					-2 => Validate cart
#
# Expects:	%::g_InputHash, and %g_SetupBlob
#					should be defined
#
# Returns:	0 - $ReturnCode
#				1 - $Error
#				2 - $pData (a reference to the order details)
#
#				if $ReturnCode = $::FAILURE, the operation failed
#					for the reason specified in $Error
#				else $ReturnCode = $::BADDATA then $Error contains
#					the order detail page HTML modified to
#					correct the order
#				else $::SUCCESS then $pData contains the data
#					the page
#
#######################################################

sub ValidateOrderDetails
	{
#? ACTINIC::ASSERT($#_ <= 1, "Invalid argument count in ValidateOrderDetails ($#_)", __LINE__, __FILE__);
	my ($pOrderDetails) = shift;
	my $nIndex;
	my %hFailures;

	if (defined $_[0])
		{
		$nIndex = $_[0];
		}
	else
		{
		$nIndex = -1;
		}

	my ($bInfoExists, $bDateExists, $key, $value, $sMessage, %Values);
	$bInfoExists = $::FALSE;
	$bDateExists = $::FALSE;
	$sMessage = "";
	#
	# Locate the product of interest
	#
	my ($pProduct);
	my $ProductRef = $$pOrderDetails{"PRODUCT_REFERENCE"};
	#
	# Locate section blob
	#
	my ($Status, $Message, $sSectionBlobName) = ACTINIC::GetSectionBlobName($$pOrderDetails{SID}); # retrieve the blob name
	if ($Status == $::FAILURE)
		{
		return ($Status, $Message);
		}
	#
	# Locate product details
	#
	my @Response = ACTINIC::GetProduct($ProductRef, $sSectionBlobName, ACTINIC::GetPath());		# get this product object
	($Status, $Message, $pProduct) = @Response;
	if ($Status != $::SUCCESS)
		{
		return (@Response);
		}

	$bInfoExists = (length $$pProduct{"OTHER_INFO_PROMPT"} != 0); # see if the info field exists.
	$bDateExists = (length $$pProduct{"DATE_PROMPT"} != 0); # see if the date field exists
	#
	# Check if there are any variants
	#
	my ($sInfo);
	if ($bInfoExists)										# if the info prompt exists, it must contain data
		{
		@Response = InfoValidate($ProductRef , $$pOrderDetails{"INFOINPUT"}, $$pProduct{"OTHER_INFO_PROMPT"});
		if ($Response[0] != $::SUCCESS)
			{
			$sMessage .= $Response[1];
			$hFailures{"INFOINPUT"} = 1;
			$hFailures{"BAD_INFOINPUT"} = $$pOrderDetails{"INFOINPUT"};
			}
		}

	my ($nDay, $nMonth, $nYear, $bBad);
	$bBad = $::FALSE;
	if ($bDateExists)										# if the date prompt exists, confirm that the date isn't wacky
		{
		$nDay = substr($$pOrderDetails{"DATE"}, 8, 2);		# retrieve the date components from the compiled date value
		$nMonth = substr($$pOrderDetails{"DATE"}, 5, 2);	# which is in actinic internal format YYYY/MM/DD
		$nYear = substr($$pOrderDetails{"DATE"}, 0, 4);
		#
		# NOTE: the UI limits the day input to 1-31, so there is no need to check the majority of the months
		#
		if ( ($nMonth == 4 ||							# 30 day months (April, June, September, November)
				$nMonth == 6 ||
				$nMonth == 6 ||
				$nMonth == 11)  &&
			  $nDay > 30)
			{
			$bBad = $::TRUE;
			}
		elsif ($nMonth == 2)								# 28/29 day month
			{
			if ($nDay > 29)								# if the day is more than 29
				{
				$bBad = $::TRUE;							# definitely a problem
				}
			elsif ($nDay == 29)							# if the day is exactly 29
				{
				if ($nYear % 400 == 0)					# if this is the fourth century
					{
																# no-op, leap year is OK on years divisible by 400
					}
				elsif ($nYear % 100 == 0)				# if the is century 1-3
					{
					$bBad = $::TRUE;						# leap year is skipped on 3 out of the 4 century years
					}
				elsif ($nYear % 4 == 0)					# this is not a century, but is divisble by 4
					{
																# no-op, leap year OK
					}
				else											# all other years, leap year bad
					{
					$bBad = $::TRUE;							# leap year is skipped on 3 out of the 4 years
					}
				}
			}

		my $sPrompt = $$pProduct{"DATE_PROMPT"};		# warn and reprompt
		if (length $nDay == 0 ||						# if any of the date fields are undefined
			 length $nMonth == 0 ||
			 length $nYear == 0)
			{
			$sMessage .= ACTINIC::GetPhrase(-1, 57, "<B>$sPrompt</B>") . "<P>\n";
			$hFailures{"DATE"} = 1;
			$hFailures{"BAD_DATE"} = $$pOrderDetails{"DATE"};
			}
		elsif ($bBad)										# an error was found
			{
			$sMessage .= ACTINIC::GetPhrase(-1, 58, "<B>$sPrompt</B>") . "<P>\n";
			$hFailures{"DATE"} = 1;
			$hFailures{"BAD_DATE"} = $$pOrderDetails{"DATE"};
			}
		}

	my ($nQuantity, $nMaxQuantity, $nMinQuantity, $nOrderedQuantity);
	$nMinQuantity = $$pProduct{"MIN_QUANTITY_ORDERABLE"}; # get the min quantity count.  this is maintained on a per
																# order detail basis
	($Status, $Message, $nMaxQuantity) = GetMaxRemains($ProductRef, $sSectionBlobName, $nIndex);	# calculate the maximum quantity of this item that can be added to the cart
	if ($Status != $::SUCCESS)
		{
		return($Status, $Message, 0, 0);
		}
	#
	# we have to summarize product quantities in the cart
	# for the proper validation
	#
	my ($pProductQuantities);
	($Status, $Message, $pProductQuantities) = CalculateCartQuantities();
	$nQuantity = $$pOrderDetails{"QUANTITY"};		# Quantity of this item (if any)
	$nOrderedQuantity = $$pProductQuantities{$$pOrderDetails{"PRODUCT_REFERENCE"}};	# total in cart
	if ($nIndex > -1)										# if editing an item
			{
		 $nOrderedQuantity -= $nQuantity;			# exclude this item
			}
	if ($nOrderedQuantity < $$pProduct{"MIN_QUANTITY_ORDERABLE"})	# Less than minimum ordered
		{
		$nMinQuantity = $$pProduct{"MIN_QUANTITY_ORDERABLE"} - $nOrderedQuantity;	# Minimum is minimum less ordered
		}
	else
		{
		$nMinQuantity = 1;								# Reached minimum so minimum now is just 1
		}
	if ($nMaxQuantity == 0)								# sold out
		{
		$sMessage .= ACTINIC::GetPhrase(-1, 59) . "<P>\n";
		$hFailures{"QUANTITY"} = 1;
		$hFailures{"BAD_QUANTITY"} = $$pOrderDetails{"QUANTITY"};
		}
	elsif ($nQuantity =~ /\D/ ||						# if there are any non-digits in the quantity
			 $nQuantity < $nMinQuantity  ||			# or the quantity is not >= min quantity
			  $nQuantity > $nMaxQuantity)
		{
		if ($nMaxQuantity > 1)
			{
			$sMessage .= ACTINIC::GetPhrase(-1, 60, $nMinQuantity, $nMaxQuantity) . "<P>\n";
			$hFailures{"QUANTITY"} = 1;
			$hFailures{"BAD_QUANTITY"} = $$pOrderDetails{"QUANTITY"};
			}
		elsif ($nMaxQuantity == 1)
			{
			$sMessage .= ACTINIC::GetPhrase(-1, 61) . "<P>\n";
			$hFailures{"QUANTITY"} = 1;
			$hFailures{"BAD_QUANTITY"} = $$pOrderDetails{"QUANTITY"};
			}
		elsif ($nMaxQuantity == -1)
			{
			$sMessage .= ACTINIC::GetPhrase(-1, 62, $nMinQuantity) . "<P>\n";
			$hFailures{"QUANTITY"} = 1;
			$hFailures{"BAD_QUANTITY"} = $$pOrderDetails{"QUANTITY"};
			}
		}
	#
	# Check is out of stock
	#
	if ($$pProduct{'OUT_OF_STOCK'})
		{
		$sMessage .= ACTINIC::GetPhrase(-1, 297, $$pProduct{'NAME'}) . "<P>\n";
		$hFailures{"QUANTITY"} = 1;
		$hFailures{"BAD_QUANTITY"} = $$pOrderDetails{"QUANTITY"};
		}
	#
	# Validate the shipping and tax info if it exists and we are not editing a cart item
	#
	if ($$::g_pSetupBlob{'TAX_AND_SHIP_EARLY'} &&# early validation is required and
		$nIndex == -1)										# we are not editing a cart item
		{
		#
		# Add temporarily the new order to the shopping cart
		# so that to be able to validate the new cart.
		# Remove the added item after finishing the validation
		#
		my ($nStatus, $sError, $pCartObject) = $::Session->GetCartObject();
		if($nStatus == $::SUCCESS)
			{
			$pCartObject->AddItem($pOrderDetails);
			my $pCartList = $pCartObject->GetCartList();	# get the shopping cart
			my $nLastItemIdx = $#$pCartList;					# get the index of the added item
			#
			# Validate shipping and tax
			#
		$sMessage .= ActinicOrder::ValidatePreliminaryInfo($::TRUE);
#? ACTINIC::ASSERT($nLastItemIdx == $#$pCartList, "Item count has been changed during validation.", __LINE__, __FILE__);
			$pCartObject->RemoveItem($nLastItemIdx);		# remove the new item as it is not fully validated yet
			}
		}
	#
	# If there were any error and the index is specified then add
	# the product name to the error message
	#
	if (length $sMessage > 0 &&
		 $nIndex > -1)
		{
		$sMessage = "<B>" . $$pProduct{"NAME"} . ":</B><BR><BLOCKQOUTE>" . $sMessage . "</BLOCKQOUTE>";
		}
	return (length $sMessage == 0 ? $::SUCCESS : $::BADDATA, $sMessage, \%hFailures, 0);
	}

#######################################################
#
# GetMaxRemains - Calculate the maximum number of items
#  orderable.
#
# Params:	[0] - product ref
#				[1] - section blob name
#				[2] - (optional) index of item in cart
#						that is being edited
#					-1 => Adding quantity (default)
#					-2 => Validate cart
#
# Returns:	($ReturnCode, $Error, $nQuantity)
#				if $ReturnCode = $::FAILURE, the operation failed
#					for the reason specified in $Error
#				else $::SUCCESS then $nQuantity contains
#					-1 - no more items can be ordered
#					0 - no limit on items count
#					N - (positive) max number of items
#						that can be ordered
#
#######################################################

sub GetMaxRemains
	{
	no strict 'refs';
	if ($#_ < 1)
		{
		return ($::FAILURE, ACTINIC::GetPhrase(-1, 12, 'GetMaxRemains'), 0, 0);
		}

	my (@Response, $Status, $Message, $sCartID, $ProductRef, $Product, $nIndex, $sSectionBlob);
	$ProductRef = $_[0];
	$sSectionBlob = $_[1];
	if (defined $_[2])
		{
		$nIndex = $_[2];
		}
	else
		{
		$nIndex = -1;
		}

	@Response = ACTINIC::GetProduct($ProductRef, $sSectionBlob,
		ACTINIC::GetPath());							# get this product object
	my ($pProduct);
	($Status, $Message, $pProduct) = @Response;
	if ($Status == $::NOTFOUND)						# the item has been removed from the catalog
		{
		# no-op, deleted product works fine here
		}
	if ($Status == $::FAILURE)
		{
		return (@Response);
		}

	@Response = $::Session->GetCartObject();
	my $pCartObject = $Response[2];

	my $pCartList = $pCartObject->GetCartList();

	my ($OrderDetail, %CurrentItem, $nQuantityLeft, $nCartItemIndex);
	$nQuantityLeft = $$pProduct{'MAX_QUANTITY_ORDERABLE'};
	if ($nQuantityLeft  == 0)						# if there is no limit on the quantity ordered
		{
		$nQuantityLeft = $::MAX_ORD_QTY;				# use integer maximum
		}
	if ($nIndex != -2)
		{
	$nCartItemIndex = -1;								# index of the current cart item
	foreach $OrderDetail (@$pCartList)				# review all of the items in the cart
		{
		%CurrentItem = %$OrderDetail;					# get the next item

		$nCartItemIndex++;								# keep track of the current item index

		if ($nCartItemIndex == $nIndex)				# if this item is the one being edited,
			{
			next;												# skip it
			}

		if ($CurrentItem{'PRODUCT_REFERENCE'} eq $ProductRef)	# if this cart item matches the selected product
			{
			$nQuantityLeft -= $CurrentItem{'QUANTITY'};	# sum the total items ordered
			}
		}
		}

	if ($nQuantityLeft < 0)								# if the max is already bought,
		{
		$nQuantityLeft = -1;								# -1 indicates error
		}

	return ($::SUCCESS, "", $nQuantityLeft, 0);
	}

##############################################################################################################
#
# Other info prompt related processing - BEGIN
#
##############################################################################################################

#######################################################
#
# InfoHTMLGenerate - Generates HTML code of the info field
#	 for the given product reference
#
# Arguments:	0 - reference of the product (HTML generated for)
#					1 - item index (equal to prod ref on product page)
#					2 - string value of the content
#					3 - static/editable indicator
#					4 - higlight?
#
# Returns:	0 - the generated HTML source
#
#######################################################

sub InfoHTMLGenerate
	{
	my $sProdref = shift;								# the reference of the product
	my $nIndex 	= shift;									# index of the field
	my $sValue 	= shift;									# the initial value of the info (as standard string)
	my $bStatic	= shift;									# static or editable HTML?
	my $bHighLight = shift;
	my $sHTML;												# the HTML code of the related edit box
	#
	# The default processing doesn't depend on the product reference
	# If you want product specific prodessing the edit the line
	# below as appropriate
	#
	if ($bStatic)
		{
		$sHTML = $sValue;
		}
	else
		{
		my $sStyle;
		if (defined $bHighLight &&
		    $bHighLight == $::TRUE)
			{
			$sStyle = " style=\"background-color: $::g_sErrorColor\"";
			}
		my $sIndex = "O_$nIndex";
		$sHTML = ACTINIC::GetPhrase(-1, 2161, "", $sIndex, 35, 1000, $sValue, $sStyle);
		}
	#############################################################
	#
	# CUSTOMISATION BEGIN
	#
	#############################################################
	#
	# Alternate processing:
	# If you would like two info prompt fields for a given product
	# then your custom code should look like:
	#
	# 	if ($sProdref eq "5")
	#		{
	#		my @aValues = split /\|\|\|/, $sValue;
	#		if ($bStatic)
	#			{
	#			$sHTML = join "<BR>", @aValues;
	#			}
	#		else
	#			{
	#			$sHTML = "<INPUT TYPE=TEXT SIZE=\"80\" NAME=\"O_1_$nIndex\" VALUE=\"" . $aValues[0] . "\"><BR>";
	#			$sHTML .= "<INPUT TYPE=TEXT SIZE=\"80\" NAME=\"O_2_$nIndex\" VALUE=\"" . $aValues[1] . "\">";
	#			}
	#		}
	#
	# Note: the functions below should also reflect your customisation
	#
	#############################################################
	#
	# CUSTOMISATION END
	#
	#############################################################
	return $sHTML;
	}

#######################################################
#
# InfoHTMLGenerate - Generates HTML code of the info field
#	 for the given product reference
#
# Arguments:	0 - reference of the product
#					1 - item index (equal to prod ref on product page)
#
# Returns:	0 - the string value of the info prompt
#
#######################################################

sub InfoGetValue
	{
	my $sProdref = shift;								# the reference of the product
	my $nIndex = shift;									# index of the field
	my $sValue;
	#
	# The default processing doesn't depend on the product reference
	# If you want product specific prodessing the edit the line
	# below as appropriate
	#
	$sValue = $::g_InputHash{"O_$nIndex"};
	#############################################################
	#
	# CUSTOMISATION BEGIN
	#
	#############################################################
	# Alternate processing:
	# If you would like two info prompt fields for a given product
	# then your custom code should look like:
	#
	# 	if ($sProdref eq "5")
	#		{
	#		$sValue = $::g_InputHash{"O_1_$nIndex"};
	#		$sValue .= "|||" . $::g_InputHash{"O_2_$nIndex"};
	#		}
	#############################################################
	#
	# CUSTOMISATION END
	#
	#############################################################
	$sValue =~ s/\n/%0a/g;								# Preserve new lines
	return $sValue;
	}

#######################################################
#
# InfoValidate - Validate the info prompt
#	 for the given product reference
#
# Arguments:	0 - reference of the product
#					1 - value of the field
#
# Returns:	0 - the validation result
#				1 - error message if any
#
#######################################################

sub InfoValidate
	{
	my $sProdref = shift;								# the reference of the product
	my $sInfo	= shift;									# the value of the field
	my $sPrompt	= shift;									# the prompt for the field
	my $sMessage;
	#
	# The default processing doesn't depend on the product reference
	# If you want product specific prodessing the edit the line
	# below as appropriate
	#
	if (length $sInfo == 0)							# if there is no info, reprompt
		{
		$sMessage .= ACTINIC::GetPhrase(-1, 55, "<B>$sPrompt</B>") . "<P>\n";
		}
	elsif (length $sInfo > 1000)
		{
		$sMessage .= ACTINIC::GetPhrase(-1, 56, "<B>$sPrompt</B>") . "<P>\n";
		}
	#############################################################
	#
	# CUSTOMISATION BEGIN
	#
	#############################################################
	# Alternate processing:
	# If you have two info prompt fields and both of them should
	# be at least 5 chars long then your custom code should look
	# like:
	#
	#	my @aValues = split /\|\|\|/, $sInfo;
	#	if (length $aValues[0] < 5 ||
	#		 length $aValues[1] < 5)
	#		{
	#		$sMessage = "The prompt should be at least 5 charaters long";
	#		}
	#############################################################
	#
	# CUSTOMISATION END
	#
	#############################################################
	return (length $sMessage == 0 ? $::SUCCESS : $::BADDATA, $sMessage);
	}

##############################################################################################################
#
# Other info prompt related processing - END
#
##############################################################################################################

#######################################################
#
# ActinicLocations - package encapsulating location information
#				for third-party applications
#
# Care should be taken that the interface does not change.
#
#######################################################

package ActinicLocations;

#######################################################
#
# GetISOCountryCode - gets an ISO country code corresponding
#				to an actinic country code
#
# Params:	0 - Actinic country code
#
# Returns:	0 - ISO country code
#
#######################################################
#   WARNING WARNING WARNING WARNING WARNING WARNING
#######################################################
#
# Any modification of this function's interface or
# basic functionality have to be reflected in the
# PSP plug ins!!!
# 20 Feb 2002 gmenyhert
#
#######################################################
#   WARNING WARNING WARNING WARNING WARNING WARNING
#######################################################

sub GetISOCountryCode
	{
	my ($sActinicCode) = @_;
	#
	# Just convert UK to GB
	#
	if (uc($sActinicCode) eq 'UK')
		{
		return('GB');
		}
	return($sActinicCode);
	}

#######################################################
#
# GetISORegionCode - gets an ISO region code corresponding
#				to an actinic region code
#
# Params:	0 - Actinic region code
#
# Returns:	0 - ISO region code
#
#######################################################

sub GetISORegionCode
	{
	my ($sActinicCode) = @_;
	#
	# Actinic region codes are in the format: <COUNTRY CODE>.<REGION CODE>[:<DISTRICT CODE>]
	#
	my $sISOCode = $sActinicCode;
	#
	# Strip off the country specifier
	#
	if($sISOCode =~ /^(\w+)\.(.+)/)
		{
		$sISOCode = $2;
		}
	#
	# Strip off the district specifier
	#
	if($sISOCode =~ /^(\w+):(.+)/)
		{
		$sISOCode = $1;
		}
	return($sISOCode);
	}

#######################################################
#
# GetMerchantCountryCode - Gets the merchant country code
#
# Returns:	0 - merchant Actinic country code
#
#######################################################
#   WARNING WARNING WARNING WARNING WARNING WARNING
#######################################################
#
# Any modification of this function's interface or
# basic functionality have to be reflected in the
# PSP plug ins!!!
# 20 Feb 2002 gmenyhert
#
#######################################################
#   WARNING WARNING WARNING WARNING WARNING WARNING
#######################################################

sub GetMerchantCountryCode
	{
#? ACTINIC::ASSERT(!defined $$::g_SetupBlob{MERCHANT_COUNTRY_CODE}, "MERCHANT_COUNTRY_CODE is undefined.", __LINE__, __FILE__);
	#
	# Get the code from the setup blob
	#
	return($$::g_pSetupBlob{MERCHANT_COUNTRY_CODE});
	}

#######################################################
#
# GetISODeliveryCountryCode - Gets the ISO delivery country code
#
# Returns:	0 - delivery ISO country code
#
#######################################################

sub GetISODeliveryCountryCode
	{
	#
	# Get the code from the location info hash and convert to ISO code
	#
	return(GetISOCountryCode($::g_LocationInfo{DELIVERY_COUNTRY_CODE}));
	}

#######################################################
#
# GetISOInvoiceCountryCode - Gets the ISO invoice country code
#
# Returns:	0 - delivery ISO country code
#
#######################################################
#   WARNING WARNING WARNING WARNING WARNING WARNING
#######################################################
#
# Any modification of this function's interface or
# basic functionality have to be reflected in the
# PSP plug ins!!!
# 20 Feb 2002 gmenyhert
#
#######################################################
#   WARNING WARNING WARNING WARNING WARNING WARNING
#######################################################

sub GetISOInvoiceCountryCode
	{
	#
	# Get the code from the location info hash and convert to ISO code
	#
	return(GetISOCountryCode($::g_LocationInfo{INVOICE_COUNTRY_CODE}));
	}

#######################################################
#
# GetISODeliveryRegionCode - Gets the ISO delivery country code
#
# Returns:	0 - delivery ISO region code
#
#######################################################
#   WARNING WARNING WARNING WARNING WARNING WARNING
#######################################################
#
# Any modification of this function's interface or
# basic functionality have to be reflected in the
# PSP plug ins!!!
# 20 Feb 2002 gmenyhert
#
#######################################################
#   WARNING WARNING WARNING WARNING WARNING WARNING
#######################################################

sub GetISODeliveryRegionCode
	{
	#
	# Get the code from the location info hash and convert to ISO code
	#
	return(GetISORegionCode($::g_LocationInfo{DELIVERY_REGION_CODE}));
	}

#######################################################
#
# GetISOInvoiceRegionCode - Gets the ISO invoice country code
#
# Returns:	0 - delivery ISO region code
#
#######################################################

sub GetISOInvoiceRegionCode
	{
	#
	# Get the code from the location blob and strip any country and district information
	#
	return(GetISORegionCode($::g_LocationInfo{INVOICE_REGION_CODE}));
	}

#######################################################
#
# GetDeliveryAddressRegionName - Gets the delivery state/province
#				for address purposes
#
# Params:	0 - current region name
#
# Returns:	0 - delivery region address name
#
#######################################################

sub GetDeliveryAddressRegionName
	{
	my ($sRegionName) = @_;
	#
	# If we have a code for the state/province, strip any sub-region identifier
	#
	if	(defined $::g_LocationInfo{DELIVERY_REGION_CODE} &&								# if the delivery region is present
	   $::g_LocationInfo{DELIVERY_REGION_CODE} ne '' &&									# and the delivery state is undefined
	   $::g_LocationInfo{DELIVERY_REGION_CODE} ne $ActinicOrder::UNDEFINED_REGION)# and the delivery state is undefined
		{
		return(GetAddressRegionName($::g_LocationInfo{DELIVERY_REGION_CODE}));
		}
	#
	# Return the existing field
	#
	return($sRegionName);
	}

#######################################################
#
# GetInvoiceAddressRegionName - Gets the invoice state/province
#				for address purposes
#
# Params:	0 - current region name
#
# Returns:	0 - invoice region address name
#
#######################################################

sub GetInvoiceAddressRegionName
	{
	my ($sRegionName) = @_;
	#
	# If we have a code for the state/province, strip any sub-region identifier
	#
	if	(defined $::g_LocationInfo{INVOICE_REGION_CODE} &&									# if the INVOICE region is present
	   $::g_LocationInfo{INVOICE_REGION_CODE} ne '' &&										# and the INVOICE state is undefined
	   $::g_LocationInfo{INVOICE_REGION_CODE} ne $ActinicOrder::UNDEFINED_REGION)	# and the INVOICE state is undefined
		{
		return(GetAddressRegionName($::g_LocationInfo{INVOICE_REGION_CODE}));
		}
	#
	# Return the existing field
	#
	return($sRegionName);
	}

#######################################################
#
# GetAddressRegionName - Gets the invoice state/province
#
# Input:	0 - Actinic region code
#
# Returns:	0 - invoice region name
#
#######################################################

sub GetAddressRegionName
	{
	my ($sActinicCode) = @_;
	my $sRegionName = ACTINIC::GetCountryName($sActinicCode);
	#
	# Actinic sub-region codes are in the format: <COUNTRY CODE>.<REGION CODE>[:<DISTRICT CODE>]
	# Actinic sub-region names are in the format: <REGION ADDRESS NAME>[:<DISTRICT NAME>]
	#
	# Check if this is a sub-region
	#
	if($sActinicCode =~ /:(\w+)$/)
		{
		#
		# Discard a colon and anything after it plus any white space before it
		#
		$sRegionName =~ s/\s*:.+$//;
		}
	return($sRegionName);
	}

#######################################################
#
# GetParentRegionCode - Gets the parent state/province
#			region code
#
# Input:	0 - Actinic region code
#
# Returns:	0 - invoice region name
#
#######################################################

sub GetParentRegionCode
	{
	my ($sActinicCode) = @_;
	#
	# Actinic sub-region codes are in the format: <COUNTRY CODE>.<REGION CODE>[:<DISTRICT CODE>]
	# Actinic sub-region names are in the format: <REGION ADDRESS NAME>[:<DISTRICT NAME>]
	#
	# Check if this is a sub-region
	#
	if($sActinicCode =~ /:/)
		{
		#
		# Discard the colon and anything after it
		#
		$sActinicCode =~ s/:.+$//;
		}
	return($sActinicCode);
	}

#######################################################
#
# GetInvoiceParentRegionCode - Gets the invoice parent state/province
#			region code
#
# This strips any sub-state specifier from the invoice region code
#
# Returns:	0 - invoice region name or empty string
#
#######################################################

sub GetInvoiceParentRegionCode
	{
	if	(defined $::g_LocationInfo{INVOICE_REGION_CODE} &&									# if the invoice region is present
	   $::g_LocationInfo{INVOICE_REGION_CODE} ne '' &&										# and the invoice state is undefined
	   $::g_LocationInfo{INVOICE_REGION_CODE} ne $ActinicOrder::UNDEFINED_REGION)	# and the invoice state is undefined
		{
		return(GetParentRegionCode($::g_LocationInfo{INVOICE_REGION_CODE}));
		}
	return('');
	}

#######################################################
#
# GetDeliveryParentRegionCode - Gets the invoice parent state/province
#			region code
#
# This strips any sub-state specifier from the invoice region code
#
# Returns:	0 - invoice region name or empty string
#
#######################################################

sub GetDeliveryParentRegionCode
	{
	if	(defined $::g_LocationInfo{DELIVERY_REGION_CODE} &&									# if the delivery region is present
	   $::g_LocationInfo{DELIVERY_REGION_CODE} ne '' &&										# and the delivery state is undefined
	   $::g_LocationInfo{DELIVERY_REGION_CODE} ne $ActinicOrder::UNDEFINED_REGION)	# and the delivery state is undefined
		{
		return(GetParentRegionCode($::g_LocationInfo{DELIVERY_REGION_CODE}));
		}
	return('');
	}

##############################################################################################################
#
# ActinicLocations package - End
#
##############################################################################################################

1;
