################################################################ # # ShippingTemplate.pl - code part of Shipping # # *** Do not change this code unless you know what you are doing *** # # Written by Kevin Grumball # Revised by Mike Purnell November 2001 # # Copyright (c) Actinic Software Ltd 1998-2001 All rights reserved # # This script is called by an eval() function and it will already # have the following variables set up: # # 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, # GetHandlingDescription, 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_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 # # $Revision: 101 $ # ################################################################ use strict; #? my @__keys1 = keys %::g_InputHash; #? ACTINIC::ASSERT($#__keys1 != -1, 'Input has undefined', __LINE__, __FILE__); #? my @__keys2 = keys %::s_Ship_PriceFormatBlob; #? ACTINIC::ASSERT($#__keys2 != -1, 'Price object undefined', __LINE__, __FILE__); my $UNDEFINED = 'UndefinedRegion'; # undefined region flag # # Add a variable to hold the online error handling if any # my $sOnlineError = ''; # # UPS constants # $::UPS_XPCI_VERSION = '1.0001'; # # UPS status codes # $::UPS_SUCCESSFUL = '1'; $::UPS_FAILED = '0'; # # UPS node names # $::XML_HEADER = ""; $::UPS_XML_RESPONSE = 'Response'; $::UPS_XML_RESPONSE_STATUS_CODE = 'ResponseStatusCode'; $::UPS_XML_RESPONSE_STATUS_DESCRIPTION = 'ResponseStatusDescription'; $::UPS_XML_ERROR = 'Error'; $::UPS_XML_ERROR_DESCRIPTION = 'ErrorDescription'; $::UPS_XML_ERROR_SEVERITY = 'ErrorSeverity'; $::UPS_XML_ADDRESS_VALIDATION_RESULT = 'AddressValidationResult'; $::UPS_XML_RATED_SHIPMENT = 'RatedShipment'; $::UPS_XML_SERVICE = 'Service'; $::UPS_XML_SERVICE_CODE = 'Code'; $::UPS_XML_TOTAL_CHARGES = 'TotalCharges'; $::UPS_XML_CURRENCY_CODE = 'CurrencyCode'; $::UPS_XML_MONETARY_VALUE = 'MonetaryValue'; $::UPS_XML_RANK = 'Rank'; $::UPS_XML_QUALITY = 'Quality'; $::UPS_XML_ADDRESS = 'Address'; $::UPS_XML_STATE_PROVINCE_CODE = 'StateProvinceCode'; $::UPS_XML_CITY = 'City'; $::UPS_XML_POSTAL_CODE_LOW_END = 'PostalCodeLowEnd'; $::UPS_XML_POSTAL_CODE_HIGH_END = 'PostalCodeHighEnd'; $::UPS_ERROR_SEVERITY_TRANSIENT_ERROR = 'Transient'; $::UPS_ERROR_SEVERITY_HARD_ERROR = 'Hard'; $::UPS_ERROR_SEVERITY_WARNING = 'Warning'; # # SSL Connection for UPS communication # my $ssl_socket; # # initialize the response variables # %::s_Ship_nShippingStatus = (); %::s_Ship_sShippingError = (); %::s_Ship_PreliminaryInfoVariables = (); %::s_Ship_ShippingVariables = (); $::s_Ship_bPrelimIsHidden = $::FALSE; $::s_Ship_bShipPhaseIsHidden = $::FALSE; $::s_Ship_sShippingDescription = ''; $::s_Ship_sHandlingDescription = ''; # not used in this plug-in $::s_Ship_sShippingCountryName = ''; $::s_Ship_nShipCharges = 0; $::s_Ship_nShippingStatus{GetHandlingDescription} = $::SUCCESS; $::s_Ship_sShippingError{GetHandlingDescription} = ''; $::s_Ship_bDisplayExtraCartInformation = $::FALSE; %::s_Ship_hShippingClassProviderIDs = (); %::s_Ship_hBasePlusPerProviderIDs = (); $::s_Ship_nSSPProviderID = -1; # # Remember if # - there was no UPS classes added to the shipping service classes # - there were base plus per classes added to the shipping classes due to a server connection failure # - there were UPS classes added to the shipping classes # $::UPS_CLASSES_NOT_USED = 0; $::UPS_CLASSES_USED = 1; $::UPS_BASEPLUSPER_CLASSES_USED = 2; my %hSSPUsed; # # Handling UPS unavailability # my $bUPS_Available = $::TRUE; # # define the string for confirm by email shipping # my $sCONFIRM_BY_EMAIL = 'Actinic:ConfirmByEmail'; # # Define our array of valid classes # local @::s_arrSortedShippingHashes; # # Define a hash of our current selection as specified by # the contents of the opaque data # local %::s_hashShipData; # # Define a hash of class IDs to weight/cost entries # local %::s_hashClassToWeightCost; # # Define our array of functions to be called # in sequence # my @arrFuncns = ( [\&ValidatePreliminaryInput, 'ValidatePreliminaryInput'], [\&ValidateFinalInput, 'ValidateFinalInput'], [\&RestoreFinalUI, 'RestoreFinalUI'], [\&CalculateShipping, 'CalculateShipping'], [\&IsFinalPhaseHidden, 'IsFinalPhaseHidden'], [\&GetShippingDescription, 'GetShippingDescription'], [\&CalculateHandling, 'CalculateHandling'], ); # # Get the current selection into a hash # OpaqueToHash(); # # Do the actual processing # my ($parrFunction, $nReturnCode, $sError); $nReturnCode = $::SUCCESS; # make sure we start foreach $parrFunction (@arrFuncns) # for each function in the array { # if($nReturnCode == $::SUCCESS) # if the previous function succeeded { my $pFunction = $$parrFunction[0]; ($nReturnCode, $sError) = &$pFunction(); # call this function } # # Save the status and error if any # $::s_Ship_nShippingStatus{$$parrFunction[1]} = $nReturnCode; $::s_Ship_sShippingError{$$parrFunction[1]} = $sError; } SaveSelectionToOpaqueData(); return($::SUCCESS); # abort execution (the $::SUCCESS here indicates that the script did not crash) #------------------------------------------------------ # # High-level functions # #------------------------------------------------------ ####################################################### # # ValidatePreliminaryInput - Validate the user # selection at the preliminary level and filter out # any special cases if we can identify them # # Returns: 0 - status # 1 - message (if any) # ####################################################### sub ValidatePreliminaryInput { # # If it's simple shipping then just return. Simple shipping has no preliminary # input. # if ($ShippingBasis eq 'Simple') # if simple shipping { return($::SUCCESS, undef); } # # Advanced shipping # # Check if we qualify for free shipping # if ($WaiveCharges eq 'Value' && # we support free over CalculatePrice() > $WaiveThreshold) # and we've exceeded the threshold { return(SetFreeShipping()); } # # If we don't know the country, shipping is undefined # if($::s_sDeliveryCountryCode eq '') { return(SetUndefinedShipping()); } # # If they selected None of the Above, we apply a default charge if # allowed otherise return an error # if($::s_sDeliveryCountryCode eq $ActinicOrder::REGION_NOT_SUPPLIED) { return(SetDefaultCharge()); } # # We've handled an unknown country and None of the Above, so we # must have a valid country # # Make sure that they have selected a state if this country has states and requires them. # They do not need to select a state if the country has no states or if the country is in # a zone that none of its states are in. # if ($::s_sDeliveryRegionCode eq "" || # if the state is undefined $::s_sDeliveryRegionCode eq $UNDEFINED) { if (defined $ParentZoneTable{$::s_sDeliveryCountryCode} && # if the country has states and $#{$ParentZoneTable{$::s_sDeliveryCountryCode}} == -1) # the country requires a state to map to a zone { return ($::FAILURE, $$pMessageList[9]); # tell the user we want a state } } # # If we know the delivery country # Get the SSP providers for this country # my $pProviderList = GetSSPProviderList($::s_sDeliveryCountryCode); if (keys %ZoneTable == 0 && # if no actinic zones and @$pProviderList == 0 ) # no SSP support for this country { return(SetDefaultCharge()); # set default charge or return an error } # # If we're using online tools check the required fields # # Check AVS if enabled # if($::g_pSSPSetupBlob && $$::g_pSSPSetupBlob{1}{'AVSEnabled'}) { my $sCity = ''; if(defined $::g_InputHash{DELIVERADDRESS3}) { $sCity = $::g_InputHash{DELIVERADDRESS3}; } elsif(defined $::g_InputHash{INVOICEADDRESS3} && $::g_LocationInfo{SEPARATESHIP} eq '') { $sCity = $::g_InputHash{INVOICEADDRESS3}; } # # Do the online AVS # my ($Result, $sSSPError) = DoUPSAddressValidation(ActinicLocations::GetISODeliveryCountryCode(), ActinicLocations::GetISODeliveryRegionCode(), $sCity, $::g_LocationInfo{DELIVERPOSTALCODE}); if($Result == $::BADDATA) # note that it doesn't cover server unavailable error in which case we let the user proceed buying { # # This can occur either for state/postcode or state/city/postcode. # If just state/postcode, we can't calculate the shipping so set to # undefined # if($sCity eq '') { SetUndefinedShipping(); } return($::FAILURE, $sSSPError); } } return($::SUCCESS, undef); } ####################################################### # # ValidateFinalInput - Validate the final user # selection and return the shipping selection in # an opaque string # # Returns: 0 - status # 1 - message (if any) # ####################################################### sub ValidateFinalInput { # # If it's simple shipping then validate the input cost # if ($ShippingBasis eq 'Simple') { return(SimpleValidateFinalInput()); # validate simple } # # Advanced shipping # # If we've populated our shipping hashes with free or default shipping # there's nothing more to do # if(@::s_arrSortedShippingHashes > 0) { return($::SUCCESS, undef); } # # Calculate the multi-package shipping if we haven't hit # free, undefined or default shipping # my ($nReturnCode, $sError, $parrShipSeparatePackages, $parrMixedPackages); if(@::s_arrSortedShippingHashes == 0) { # # Calculate the (multi-package) shipping # ($nReturnCode, $sError, $parrShipSeparatePackages, $parrMixedPackages) = CalculateMultiPackageShipping(); if($nReturnCode != $::SUCCESS) { return($nReturnCode, $sError); } } SaveSelectionToOpaqueData($parrShipSeparatePackages, $parrMixedPackages); # Save the selection to the opaque data return($::SUCCESS, undef); } ####################################################### # # RestoreFinalUI - generate a hash of substitution values # The keys in the hash are strings in the shipping # HTML that need to be replaced with the corresponding # value. This function processes the final shipping UI. # # Returns: 0 - status # 1 - message (if any) # ####################################################### sub RestoreFinalUI { # # Simple mode # if ($ShippingBasis eq 'Simple') # we are in simple mode { return(SimpleRestoreFinalUI()); } # # Advanced mode # my ($phashShipping, $sClassLabel, $sClassID, $sSelectHTML); my $sPriceLabelFormat = ' (%s)'; if(@::s_arrSortedShippingHashes == 1) # if there's only one option { $phashShipping = $::s_arrSortedShippingHashes[0]; # # Handle the label by appending the cost if we're displaying prices # $sClassLabel = $$phashShipping{ShippingLabel}; if ($::s_Ship_bDisplayPrices) # displaying prices? { my (@PriceResponse) = ActinicOrder::FormatPrice($$phashShipping{Cost}, $::TRUE, \%::s_Ship_PriceFormatBlob); $sClassLabel .= sprintf($sPriceLabelFormat, $PriceResponse[2]); # add the price to the label } # # Format as a HIDDEN tag # $sSelectHTML = sprintf("%s\n", $sClassLabel, $$phashShipping{ShippingClass}); } else { # # Start the SELECT tag # $sSelectHTML = ""; # CHD moved this expression out of the quotes \n", # CHD changed the
%s
} # # Determine which trademarks, disclaimers should be displayed # if($hSSPUsed{$::UPS_CLASSES_USED} == $::TRUE) { $::s_Ship_hShippingClassProviderIDs{1} = $::TRUE; } elsif ($hSSPUsed{$::UPS_BASEPLUSPER_CLASSES_USED} == $::TRUE) { $::s_Ship_hBasePlusPerProviderIDs{1} = $::TRUE; } $::s_Ship_ShippingVariables{$::VARPREFIX . 'SHIPPINGSELECT'} = $sSelectHTML; return($::SUCCESS, undef); } ####################################################### # # CalculateShipping # Get the possible zones for this country and region # There may be more than one possible zone and we can # select the shipping band based on the class of shipping. # # Returns: 0 - status # 1 - message (if any) # ####################################################### sub CalculateShipping { # # For simple shipping, we just apply the single value # if ($ShippingBasis eq 'Simple') # Simple shipping { return(SimpleCalculateShipping()); } # # If there are no hashes in the sorted array # if(@::s_arrSortedShippingHashes == 0) { return($::SUCCESS, undef); } # # Handle a selected UPS class # if($::s_hashShipData{'ShippingClass'} =~ /^(\d+)_(.+)/) { $::s_Ship_nSSPProviderID = $1; # # Check if this is an error class # my $bSSPError = $2 eq $sCONFIRM_BY_EMAIL; my $pSSPProvider = GetUPSSetup(); $::s_Ship_sSSPOpaqueShipData = sprintf("SSPID=%d;SSPClassRef=%s;OrigZip=%s;OrigCntry=%s;OrigCntryDesc=%s;Pack=%s;Rate=%s;Weight=%.03f;DestCntry=%s;DestPost=%s;Residential=%s;", $::s_Ship_nSSPProviderID, $2, $$pSSPProvider{ShipperPostalCode}, $$pSSPProvider{ShipperCountry}, ACTINIC::GetCountryName($$pSSPProvider{ShipperCountry}), $$pSSPProvider{'PackagingType'}, $$pSSPProvider{'RateChart'}, $::s_hashShipData{BasisTotal}, $::s_sDeliveryCountryCode, $::g_ShipContact{'POSTALCODE'}, $::g_LocationInfo{DELIVERRESIDENTIAL} ne '' ? 1 : 0 ); if($::s_Ship_nSSPProviderID == 1) { if(!$bSSPError) { $::s_Ship_bDisplayExtraCartInformation = $::TRUE; } } } return($::SUCCESS, undef); # It succeeded } ####################################################### # # IsFinalPhaseHidden - is the final shipping phase # hidden. Yes if there is only one payment option # # Returns: 0 - status # 1 - message (if any) # ####################################################### sub IsFinalPhaseHidden { # # Simple mode # if ($ShippingBasis eq 'Simple') # we are in simple mode { return($::SUCCESS, undef); # default visible } # # Hide the phase if there's only one option # if(@::s_arrSortedShippingHashes == 1) { $::s_Ship_bShipPhaseIsHidden = $::TRUE; # hide the pointless phase } return($::SUCCESS, undef); # default visible } ####################################################### # # GetShippingDescription - retrieve the description # # Returns: 0 - status # 1 - message (if any) # ####################################################### sub GetShippingDescription { if(defined $::s_hashShipData{ShippingLabel}) # if we have a label defined { $::s_Ship_sShippingDescription = $::s_hashShipData{ShippingLabel}; # use it } else { $::s_Ship_sShippingDescription = ''; # empty string } return($::SUCCESS, undef); } ####################################################### # # CalculateHandling - calculate the handling value # # Returns: 0 - status # 1 - message (if any) # ####################################################### sub CalculateHandling { # # handling charges are simply a flat value plus a percentage of the shipping charge. Since Actinic stores # 2 decimal percentages as ints, the actual percentage value is the number / 100 (for decimals) / 100 (for percent) # $::s_Ship_nHandlingCharges = $nHandlingCharge + int ($::s_Ship_nShipCharges * $nHandlingProportion / $ActinicOrder::PERCENTOFFSET); # # store the current handling value in our opaque data for future reference # $::s_Ship_sOpaqueHandleData = sprintf("Handling;%d;", $::s_Ship_nHandlingCharges); return ($::SUCCESS, undef); } #------------------------------------------------------ # # End of high-level functions # #------------------------------------------------------ #------------------------------------------------------ # # SimpleXXX functions # #------------------------------------------------------ ####################################################### # # SimpleValidateFinalInput - Validate the simple shipping # final user selection and return the shipping # selection in an opaque string # # Returns: 0 - status # 1 - message (if any) # ####################################################### sub SimpleValidateFinalInput { my (@Response); if(!defined $::g_InputHash{SHIPPING}) { return($::SUCCESS, undef); } if ($::g_InputHash{SHIPPING}) { $::g_InputHash{SHIPPING} =~ s/^\s*(.*?)\s*$/$1/gs; } # # If the user has been presented with the edit control, we preserve the input intact # until it has been validated. We mark this as user input in the opaque data # by prepending 'Error-'. # if (defined $::g_InputHash{SHIPPING}) # if the shipping is defined, store its value { my $sText = (0 == length $::g_InputHash{SHIPPING}) ? ' ' : $::g_InputHash{SHIPPING}; $::s_Ship_sOpaqueShipData = sprintf("Simple;Error-%s;", $sText); # get the user value } if (!defined $::g_InputHash{'SHIPPING'} ||# if the shipping is undefined, error out length $::g_InputHash{'SHIPPING'} == 0) { return($::FAILURE, $$pMessageList[8]); } @Response = ActinicOrder::ReadPrice($::g_InputHash{SHIPPING}, \%::s_Ship_PriceFormatBlob); # make sure the price is readable if ($Response[0] != $::SUCCESS || # if the price is not readable, or $Response[2] != int $Response[2]) # it is fractional { # # format an example price # @Response = ActinicOrder::FormatSinglePrice(10000, $::FALSE, \%::s_Ship_PriceFormatBlob); if ($Response[0] != $::SUCCESS) { return($Response[0], $Response[1]); } return($::FAILURE, sprintf($$pMessageList[0], $Response[2])); } my ($nMaxShipping) = 99999999; if ($Response[2] >= $nMaxShipping) # if the shipping is too big, display error { # # format the max price # @Response = ActinicOrder::FormatPrice($nMaxShipping, $::TRUE, \%::s_Ship_PriceFormatBlob); if ($Response[0] != $::SUCCESS) { return($Response[0], $Response[1]); } return($::FAILURE, sprintf($$pMessageList[1], $Response[2])); } my ($nMinShipping) = 0; if ($Response[2] < $nMinShipping) # if the shipping is too small, display error { # # format the min price # @Response = ActinicOrder::FormatPrice($nMinShipping, $::TRUE, \%::s_Ship_PriceFormatBlob); if ($Response[0] != $::SUCCESS) { return($Response[0], $Response[1]); } return($::FAILURE, sprintf($$pMessageList[2], $Response[2])); } # # the user input must be OK so now we convert the opaque data into internal format # if (defined $::g_InputHash{SHIPPING}) # if the shipping is defined, store its value { $::s_Ship_sOpaqueShipData = sprintf("Simple;%s;", $Response[2]); # get the user value OpaqueToHash(); } return($::SUCCESS, undef); } ####################################################### # # SimpleRestoreFinalUI - generate a hash of substitution values # The keys in the hash are strings in the shipping # HTML that need to be replaced with the corresponding # value. This function processes the final shipping UI. # # Returns: 0 - status # 1 - message (if any) # ####################################################### sub SimpleRestoreFinalUI { my (@Response); # # Substitute the currency sign # my $ePosOrder = $::s_Ship_PriceFormatBlob{"ICURRENCY"}; if ($ePosOrder == 0) { $::s_Ship_ShippingVariables{"NETQUOTEVAR:CURRENCYSYMBOL1"} = $::s_Ship_PriceFormatBlob{"SCURRENCY"}; $::s_Ship_ShippingVariables{"NETQUOTEVAR:CURRENCYSYMBOL2"} = ''; } elsif ($ePosOrder == 1) { $::s_Ship_ShippingVariables{"NETQUOTEVAR:CURRENCYSYMBOL1"} = ''; $::s_Ship_ShippingVariables{"NETQUOTEVAR:CURRENCYSYMBOL2"} = $::s_Ship_PriceFormatBlob{"SCURRENCY"}; } elsif ($ePosOrder == 2) { $::s_Ship_ShippingVariables{"NETQUOTEVAR:CURRENCYSYMBOL1"} = $::s_Ship_PriceFormatBlob{"SCURRENCY"} . ' '; $::s_Ship_ShippingVariables{"NETQUOTEVAR:CURRENCYSYMBOL2"} = ''; } elsif ($ePosOrder == 3) { $::s_Ship_ShippingVariables{"NETQUOTEVAR:CURRENCYSYMBOL1"} = ''; $::s_Ship_ShippingVariables{"NETQUOTEVAR:CURRENCYSYMBOL2"} = $::s_Ship_PriceFormatBlob{"SCURRENCY"} . ' '; } # # Substitute the price # if (!defined $::s_hashShipData{'Simple'}) # shipping is still undefined { # # Format the default price. This needs to be done because the default is stored in # Actinic internal format. # @Response = ActinicOrder::FormatSinglePrice($SimpleCost, $::FALSE, \%::s_Ship_PriceFormatBlob); if ($Response[0] != $::SUCCESS) { return($Response[0], $Response[1]); } $::s_Ship_ShippingVariables{"NETQUOTEVAR:SHIPPINGVALUE"} = $Response[2]; } elsif($::s_hashShipData{'Simple'} =~ /Error-/) # there is an error in simple shipping { # # no need to format the user input since it was formatted when the entered it # $::s_hashShipData{'Simple'} =~ s/^Error-\s*(.*?)\s*$/$1/g; $::s_Ship_ShippingVariables{"NETQUOTEVAR:SHIPPINGVALUE"} = $::s_hashShipData{'Simple'}; } else # shipping is already defined { # # Valid opaque data is in Actinic format so format it as currency # $::s_hashShipData{'Simple'} =~ s/^\s*(.*?)\s*$/$1/g; @Response = ActinicOrder::FormatSinglePrice($::s_hashShipData{'Simple'}, $::FALSE, \%::s_Ship_PriceFormatBlob); if ($Response[0] != $::SUCCESS) { return($Response[0], $Response[1]); } $::s_Ship_ShippingVariables{"NETQUOTEVAR:SHIPPINGVALUE"} = $Response[2]; } return($::SUCCESS, undef); } ####################################################### # # SimpleCalculateShipping # Get the possible zones for this country and region # There may be more than one possible zone and we can # select the shipping band based on the class of shipping. # # Returns: 0 - status # 1 - message (if any) # ####################################################### sub SimpleCalculateShipping { # # For simple shipping, we just apply the single value # if (!defined $::s_hashShipData{'Simple'} || # shipping is still undefined $::s_hashShipData{'Simple'} =~ /Error-/) # or there was an error { # # Note that if the shipping is undefined we don't use the default value. Instead we # return "0" which results in the shipping fields being hidden in the shopping cart summary. # $::s_Ship_nShipCharges = 0; } else # shipping is already defined { $::s_Ship_nShipCharges = $::s_hashShipData{'Simple'}; } return($::SUCCESS, undef); } #------------------------------------------------------ # # End of SimpleXXX functions # #------------------------------------------------------ #------------------------------------------------------ # # Low-level functions # #------------------------------------------------------ ####################################################### # # GetShippingBasisTotal - Get the total based upon the shipping basis # # Returns: the basis total # ####################################################### sub GetShippingBasisTotal { my $nTotalBasis = 0; if ($ShippingBasis eq 'Quantity') # Quantity based shipping { $nTotalBasis = CalculateQuantity(); # Calculate total number of items } elsif ($ShippingBasis eq 'Price') # Price based shipping { $nTotalBasis = CalculatePrice(); # Calculate total price } elsif ($ShippingBasis eq 'Weight') # Weight-based pricing { $nTotalBasis = CalculateWeight(); # Calculate total weight } return($nTotalBasis); } ################################################################ # # CalculateWeight - get the total weight of products # # Expects: @::s_Ship_sShipProducts - List of product IDs # @::s_Ship_nShipQuantities - List of quantities (to match ProductIDs) # %::s_Ship_OpaqueDataTables - product opaque data table # $DefaultWeight - default weight to use # # Returns: Total weight # ################################################################ sub CalculateWeight { my $j; if (defined $::s_Ship_nTotalWeight) { return ($::s_Ship_nTotalWeight); } $::s_Ship_nTotalWeight = 0; for $j (0 .. $#::s_Ship_sShipProducts) { # # If we have a weight specified for that product ID # then we use it # Test for a null string so that any value- including 0 - can be used for the # product weight - a null sting is value undefined # if ("" ne $::s_Ship_OpaqueDataTables{$::s_Ship_sShipProducts[$j]}) # If we have a weight { $::s_Ship_nTotalWeight += $::s_Ship_OpaqueDataTables{$::s_Ship_sShipProducts[$j]} * $::s_Ship_nShipQuantities[$j]; } # # If not, we use the default weight # else { $::s_Ship_nTotalWeight += $DefaultWeight * $::s_Ship_nShipQuantities[$j]; # Use default weight } } return($::s_Ship_nTotalWeight); } ################################################################ # # CalculateQuantity - get the total number of products # # Expects: $::s_Ship_nTotalQuantity - the number of non-component items # # Returns: Total quantity # ################################################################ sub CalculateQuantity { #? ACTINIC::ASSERT((defined $::s_Ship_nTotalQuantity), '$::s_Ship_nTotalQuantity not defined', __LINE__, __FILE__); # # Return the total quantity # return($::s_Ship_nTotalQuantity); } ################################################################ # # CalculatePrice - get the total price of products # # Expects: @::s_Ship_sShipProducts - List of product IDs # @::s_Ship_nShipQuantities - List of quantities (to match ProductIDs) # @::s_Ship_nShipPrices - List of prices (to match ProductIDs) # # Returns: Total price of goods # ################################################################ sub CalculatePrice { my $j; if (defined $::s_Ship_nTotalPrice) { return ($::s_Ship_nTotalPrice); } if (defined $::s_Ship_nSubTotal) { return ($::s_Ship_nSubTotal); } $::s_Ship_nTotalPrice = 0; for $j (0 .. $#::s_Ship_sShipProducts) { $::s_Ship_nTotalPrice += ($::s_Ship_nShipPrices[$j] * $::s_Ship_nShipQuantities[$j]); # Add units * price } return($::s_Ship_nTotalPrice); } ####################################################### # # GetBands - retrieve the band for this region # # Returns: 0+ - band list # ####################################################### sub GetBands { if ($::s_sDeliveryRegionCode eq "" || # if the state is undefined $::s_sDeliveryRegionCode eq $UNDEFINED) { if ($#{$ParentZoneTable{$::s_sDeliveryCountryCode}} != -1) # if this parent zone table has any entries { return (@{$ParentZoneTable{$::s_sDeliveryCountryCode}}); # return this list (has invalid entries stripped) } } # # If we have a zone hash entry for the delivery country # if(defined $ZoneTable{$::s_sDeliveryCountryCode}) { # # See if there is an entry for the region code as it is # if(defined $ZoneTable{$::s_sDeliveryCountryCode}{$::s_sDeliveryRegionCode}) { return(@{ $ZoneTable{$::s_sDeliveryCountryCode}{$::s_sDeliveryRegionCode} }); } # # It failed so let's see if the location is a sub-district and try # the parent state/province # my $sParentState = ActinicLocations::GetDeliveryParentRegionCode(); if($sParentState ne '' && # if we have something $sParentState ne $::s_sDeliveryRegionCode && # and it's different from the original code defined $ZoneTable{$::s_sDeliveryCountryCode}{$sParentState}) # and there's an entry for it { return(@{ $ZoneTable{$::s_sDeliveryCountryCode}{$sParentState} }); # return the bands } # # See if there is an entry for the country code with an undefined region # if(defined $ZoneTable{$::s_sDeliveryCountryCode}{$UNDEFINED}) { return(@{ $ZoneTable{$::s_sDeliveryCountryCode}{$UNDEFINED} }); } } # # Return an empty list # my @listEmpty = (); return(@listEmpty); } ####################################################### # # GetSSPProviderList - Get the list of SSP providers for this country # # Input: $sCountryCode - country code # # Returns: 0 - list of providers # ####################################################### sub GetSSPProviderList { my ($sCountryCode) = @_; my @arrReturn; # # If we have supported regions and the delivery country is supported # get the list of providers # if(defined $$::g_pSSPSetupBlob{SupportedRegions} && defined $$::g_pSSPSetupBlob{SupportedRegions}{$sCountryCode}) { my $nProviderID; foreach $nProviderID ($$::g_pSSPSetupBlob{SupportedRegions}{$sCountryCode}) { push(@arrReturn, $nProviderID); } } return (\@arrReturn); } ####################################################### # # GetUS5DigitZipCode - Returns a 5 digit zip code or an # error if format un-recognised # # Input: $sZipCode - zip code # # Returns: 0 - $::SUCCESS or $::FAILURE # 1 - error message # 2 - 5 digit zip code # ####################################################### sub GetUS5DigitZipCode { my ($sZipCode) = @_; # # Check the US and Puerto Rico zip code is in a sensible format # if($sZipCode !~ /^\d{5}$/ && $sZipCode !~ /^\d{5}-\d{4}$/ && $sZipCode !~ /^\d{9}$/) { # # Tell buyer about US and PR zip format # return($::FAILURE, ACTINIC::GetPhrase(-1, 2150)); } # # Use the first 5 digits of the zip code # $sZipCode = substr($sZipCode, 0, 5); return($::SUCCESS, '', $sZipCode); } ################################################################ # # CalculatePackageShipping - calculate the cost of a single package # for a given zone and class # # Input: $nZoneID - the zone ID # $nClassID - the class ID # $dWeight - the weight of a package # # Returns: 1 - $::TRUE if we calculated a cost, $::FALSE if failed # 2 - the cost of the package # # Author: Mike Purnell # ################################################################ sub CalculatePackageShipping { my ($nZoneID, $nClassID, $dWeight) = @_; # # Set up our initial values # my $nCost = 0; my $bWeightOK = $::TRUE; my $dMaxWeight = 0.0; my $nHighestCost = 0; # # The ShippingTable entry for {class}{zone} is an array of hashes. The first # entry defines the excess action, the rest are {wt},{cost} entries # in ascending order # my $parrBandEntries = $ShippingTable{$nClassID}{$nZoneID}; my $nEntryCount = @$parrBandEntries; # get the number of entries in the array my $phashBandEntry; # # Get the values for the maximum weight defined # if($nEntryCount > 1) # any wt/cost entries? { $phashBandEntry = $$parrBandEntries[$nEntryCount - 1]; # get the highest weight entry $dMaxWeight = $$phashBandEntry{wt}; # store the max weight $nHighestCost = $$phashBandEntry{cost}; # and the cost for max weight } # # Check the maximum weight defined against our package weight # if($dWeight > $dMaxWeight) # exceeded max weight defined? { my $phashExcessAction = $$parrBandEntries[0]; # get the excess action hash if($$phashExcessAction{ExcessAction} eq 'Highest') # use the highest value? { $nCost = $nHighestCost; } elsif($$phashExcessAction{ExcessAction} eq 'AddFurther') # add increment? { my $dExtraWeight = $dWeight - $dMaxWeight; # get the excess weight my ($dWeightIncrement, $nChargeIncrement) = ($$phashExcessAction{'IncrementalWeight'}, $$phashExcessAction{'IncrementalCharge'}); # get the increment and incremental charge my $nExtraUnits = int ($dExtraWeight / $dWeightIncrement + 0.999); # round up the number of incremental units $nCost = $nHighestCost + # cost is highest + ($nExtraUnits * $nChargeIncrement); # extra units * incremental charge } elsif($$phashExcessAction{ExcessAction} eq 'Error') # error out? { $bWeightOK = $::FALSE; # we failed to get a cost for this weight } } else # our weight is in the band table { my $i; for($i = 1; $i < $nEntryCount; $i++) # go through the wt/cost entries in ascending order { $phashBandEntry = $$parrBandEntries[$i]; # get the wt/cost hash reference if($$phashBandEntry{wt} >= $dWeight) # inside the weight? { $nCost = $$phashBandEntry{cost}; # found our cost last; } } } return($bWeightOK, $nCost); } ################################################################ # # CalculateMultiPackageShipping - a hash of product weights to # quantity and package cost # # Expects: @::s_Ship_sShipProducts - List of product IDs # @::s_Ship_nShipQuantities - List of quantities (to match ProductIDs) # %::s_Ship_OpaqueDataTables - product opaque data table # $::s_Ship_nShipSeparately - list of ship separately flags # $DefaultWeight - default weight to use # # Returns: 0 - status # 1 - error message or '' # 2 - reference to array of single item parcels # 3 - reference to array of mixed item parcels # # Author: Mike Purnell # ################################################################ sub CalculateMultiPackageShipping { my $dWeightRemainder = 0.0; my $bNonSeparateShipFound = $::FALSE; my ($i); my $dWeight; my ($phashWeightToQuantity, $parrSortedWeightKeys, $sWeightList, $parrShipSeparatePackages, $parrMixedPackages) = DivideIntoPackages(); # split into packages # # Get the valid zone/class combinations for our location # my $parrZonesClasses = GetZoneClassCombinations(); my $pProviderList = GetSSPProviderList($::s_sDeliveryCountryCode); # # Handle no valid zone/class combinations and no valid SSP Providers for our location # if(@$parrZonesClasses == 0 && @$pProviderList == 0) { return(SetDefaultCharge()); } my $parrZoneClass; my @arrShippingHashes; foreach $parrZoneClass (@$parrZonesClasses) # go through all zone/class combinations { my $nTotalCost = 0; my ($nZoneID, $nClassID) = @$parrZoneClass; # split into zone and class my ($bWeightOK, $nPackageCost); foreach $dWeight (@$parrSortedWeightKeys) # go through our sorted weights { ($bWeightOK, $nPackageCost) = CalculatePackageShipping($nZoneID, $nClassID, $dWeight); # calculate the cost for this weight if($bWeightOK) # the weight was OK? { $nTotalCost += $$phashWeightToQuantity{$dWeight} * $nPackageCost; # add quantity * cost to total # # Add to the class to weight/cost hash # $::s_hashClassToWeightCost{$nClassID}{sprintf('%0.03f', $dWeight)} = $nPackageCost; } else # weight was too big { last; # no point going on } } if($bWeightOK) # if all weights were valid for this zone/class { push @arrShippingHashes, { 'ShippingLabel' => $ClassTable{$nClassID}, 'ShippingClass' => $nClassID, 'ShippingZone' => $nZoneID, 'Cost' => $nTotalCost, 'BasisTotal' => GetShippingBasisTotal() }; } } # # Calculate the sum of weights for further evaluation # my $dSumOfWeights = 0.0; # shows the sum of weights of all the packages foreach $dWeight (@$parrSortedWeightKeys) # go through our sorted weights { $dSumOfWeights += $$phashWeightToQuantity{$dWeight} * $dWeight; # add the weight of each package to the sum } # # Add SSP calculations # my $nProviderID; foreach $nProviderID (@$pProviderList) { # # Get weight limit information # my $bWeightThresholdExceeded = IsWeightThresholdExceeded($nProviderID, $dSumOfWeights); # determine whether there is a weight limit defined and whether the total weight exceeded that or not # # Do the rate calculation if possible # if($::g_pSSPSetupBlob && $$::g_pSSPSetupBlob{$nProviderID}{'RSSEnabled'} && $bWeightThresholdExceeded == $::FALSE) # do the calculation only if we allow UPS classes { my ($nReturnCode, $sSSPError, $parrShippingHashes, $nRateType) = GetUPSRates(); $hSSPUsed{$nRateType} = $::TRUE; if($nReturnCode != $::SUCCESS) { return($nReturnCode, $sSSPError); } else { push @arrShippingHashes, @$parrShippingHashes; } } } # # Handle no valid zone/class combinations and no valid SSP classes (e.g. due to overweight) for our location # See cix:actinic_catlog/bugs_details9:3012 # if(@$parrZonesClasses == 0 && @arrShippingHashes == 0) { return(SetDefaultCharge()); } # # If we don't have any valid classes, at least one package must exceed # the limit for all classes # if(@arrShippingHashes == 0) { return ($::FAILURE, $$pMessageList[7]); # tell the user a package is overweight } # # ACTINIC CUSTOMISE: Sort the shipping options # # If you would like to change the order in which shipping options are presented in the shipping # drop-down, comment out the line starting '@::s_arrSortedShippingHashes' and uncomment the # appropriate line # # Store the hashes in ascending order of total cost # @::s_arrSortedShippingHashes = sort{$$a{Cost} <=> $$b{Cost}} @arrShippingHashes; # # Store the hashes in descending order of total cost # # @::s_arrSortedShippingHashes = sort{$$b{Cost} <=> $$a{Cost}} @arrShippingHashes; # # Store the hashes in ascending alphabetical order # # @::s_arrSortedShippingHashes = sort{$$a{ShippingLabel} cmp $$b{ShippingLabel}} @arrShippingHashes; # # Store the hashes in descending alphabetical order # # @::s_arrSortedShippingHashes = sort{$$b{ShippingLabel} cmp $$a{ShippingLabel}} @arrShippingHashes; return($::SUCCESS, '', $parrShipSeparatePackages, $parrMixedPackages); } ################################################################ # # IsWeightThresholdExceeded - Get weight threshold value from the catalog blob if defined # # Expects: $::g_pCatalogBlob - Catalog blob # # Input: $nProviderID - ID of the provider whose classes to be added to the list # $dSumOfWeights - sum of weight of all the packages # # Returns: 0 - a bool value which specifies if a given threshold value is exceeded or not # # Author: Tibor Vajda # ################################################################ sub IsWeightThresholdExceeded { my $nProviderID = shift; # get the first parameter my $dSumOfWeights = shift; # get the second parameter # # Init variables # my $bWeightThresholdExceeded = $::FALSE; # shows whether there is a threshold defined and this is lower than the sum of package weights # # Do anything only if there is a threshold defined # if($::g_pSSPSetupBlob && $$::g_pSSPSetupBlob{$nProviderID}{'WEIGHTTHRESHOLD'}) # check if WEIGHTTHRESHOLD is defined for this provider { # # Get the threshold value from the catalog blob # my $dWeightThreshold = $$::g_pSSPSetupBlob{$nProviderID}{'WEIGHTTHRESHOLD'}; # get the weight threshold from the SSPSetup blob # # Check if the value is right # if (($dWeightThreshold ne '') && # the threshold is not empty ($dWeightThreshold =~ /^[+]?[\d]*(\.[\d]+)?$/)) # and it is a positive real number { # # Check if this order is above the limit - mind if it is # if ($dWeightThreshold < $dSumOfWeights) # if the packages exceeded the threshold weight then don't supply UPS classes { $bWeightThresholdExceeded = $::TRUE; } } } # # Pass back the result # return $bWeightThresholdExceeded; } ################################################################ # # DivideIntoPackages - Divide the order into packages # # Expects: @::s_Ship_sShipProducts - List of product IDs # @::s_Ship_nShipQuantities - List of quantities (to match ProductIDs) # %::s_Ship_OpaqueDataTables - product opaque data table # $::s_Ship_nShipSeparately - list of ship separately flags # $DefaultWeight - default weight to use # # Input: $bUseIntegralWeights - whether to use integral weights (optional) # # Returns: 0 - reference to a hash of weight to quantity # 1 - reference to an array of sorted keys # 2 - csv list of quantity@weight values # 3 - reference to array of single item parcels # 4 - reference to array of mixed item parcels # # Author: Mike Purnell # ################################################################ sub DivideIntoPackages { my ($bUseIntegralWeights) = @_; my $dWeightRemainder = 0.0; my $nNonSeparateShipCount = 0; my (%hashWeightToQuantity, @arrSortedWeightKeys); my ($i); my (@arrShipSeparatePackages, @arrMixedPackages, $parrPackage); # # We support multi-packaging if we're shipping by weight # if($ShippingBasis ne 'Weight') { my $nBasisTotal = GetShippingBasisTotal(); $hashWeightToQuantity{$nBasisTotal} = 1; # single package # # Now get the array of sorted keys # @arrSortedWeightKeys = ($nBasisTotal); return(\%hashWeightToQuantity, \@arrSortedWeightKeys, $nBasisTotal); } # # Handle multi-packaging # my $dUnitWeight; for $i (0 .. $#::s_Ship_sShipProducts) { if($::s_Ship_sShipProducts[$i] =~ /_/) # filter out components with no associated products { next; } # # If we have a weight specified for that product ID # then we use it # Test for an empty string so that any value - including 0 - can be used for the # product weight - an empty string is default weight # if ("" ne $::s_Ship_OpaqueDataTables{$::s_Ship_sShipProducts[$i]}) # If we have a weight in the opaque data { $dUnitWeight = $::s_Ship_OpaqueDataTables{$::s_Ship_sShipProducts[$i]}; # use the specified weight } else { $dUnitWeight = $DefaultWeight; # Use default weight } # # Now decide whether to ship separately based upon the flag # and the unit weight versus the optimal weight # if($::s_Ship_nShipSeparately[$i] == 1 || # this product ships separately? ($sOptimalWeight > 0 && # or we have an optimal weight? $dUnitWeight >= $sOptimalWeight)) # and this package is greater than or equal to the optimal weight? { if($bUseIntegralWeights) # if we're using integral weights { $dUnitWeight = int($dUnitWeight + 0.9999); # round up to nearest integer } # # We may already have an entry for the weight or it may be a new weight # $hashWeightToQuantity{$dUnitWeight} += $::s_Ship_nShipQuantities[$i]; # add to existing quantity # # Add the package details # my @arrTemp = ($::s_Ship_sShipProducts[$i], $::s_Ship_nShipQuantities[$i], $dUnitWeight); push @arrShipSeparatePackages, \@arrTemp; } else # ship as mixed package { $nNonSeparateShipCount += $::s_Ship_nShipQuantities[$i]; # we have a mixed package $dWeightRemainder += $dUnitWeight * $::s_Ship_nShipQuantities[$i]; # add the weight * quantity # # Add the details to the non-ship separate details # my @arrTemp = ($::s_Ship_sShipProducts[$i], $::s_Ship_nShipQuantities[$i], $dUnitWeight); push @arrMixedPackages, \@arrTemp; } } # # Add the amalgamated weight to the hash if we found any non-separate ship packages # if($nNonSeparateShipCount > 0) { my $nQuantity = 1; # # If they specfied an optimal weight, split the non-separate items into # packages # if($sOptimalWeight ne '' && $dWeightRemainder > $sOptimalWeight) { my $nCalculatedPackages = int(($dWeightRemainder / $sOptimalWeight) + 0.9999); # # If the number of calculated packages is the same as # the number of non-ship separately items, treat all items # as ship-separately # if($nCalculatedPackages == $nNonSeparateShipCount) { foreach $parrPackage (@arrMixedPackages) { $dUnitWeight = $$parrPackage[2]; if($bUseIntegralWeights) # if we're using integral weights { $dUnitWeight = int($dUnitWeight + 0.9999); # round up to nearest integer } # # We may already have an entry for the weight or it may be a new weight # $hashWeightToQuantity{$dUnitWeight} += $$parrPackage[1]; # add to existing quantity # # Add the package details # push @arrShipSeparatePackages, $parrPackage; } # # Empty the mixed packages array # @arrMixedPackages = (); } else { # # We use the minimum of the number of items and the number of calculated packages # $nQuantity = ($nCalculatedPackages < $nNonSeparateShipCount) ? $nCalculatedPackages : $nNonSeparateShipCount; # # Get the average package weight # $dWeightRemainder = $dWeightRemainder / $nQuantity; if($bUseIntegralWeights) # if we're using integral weights { $dWeightRemainder = int($dWeightRemainder + 0.9999); # round up to nearest integer } $hashWeightToQuantity{$dWeightRemainder} += $nQuantity; # add however many packages # # Add the details to the non-ship separate details # my @arrTemp = ('', $nQuantity, $dWeightRemainder); push @arrMixedPackages, \@arrTemp; } } else { if($bUseIntegralWeights) # if we're using integral weights { $dWeightRemainder = int($dWeightRemainder + 0.9999); # round up to nearest integer } $hashWeightToQuantity{$dWeightRemainder} += $nQuantity; # add however many packages # # Add the details to the non-ship separate details # my @arrTemp = ('', $nQuantity, $dWeightRemainder); push @arrMixedPackages, \@arrTemp; } } # # We sort any weights into descending order. That way we know if # a weight is invalid for a class/zone as soon as possible # @arrSortedWeightKeys = sort {$b <=> $a} keys %hashWeightToQuantity; my ($dWeight, $sWeightList); # # Format the weight/quantities as a csv list of 'qty@weight' # foreach $dWeight (@arrSortedWeightKeys) # go through our sorted weights { $sWeightList .= sprintf("%d@%.03f,", $hashWeightToQuantity{$dWeight}, $dWeight); } # # Trim the trailing comma # $sWeightList =~ s/,$//; return(\%hashWeightToQuantity, \@arrSortedWeightKeys, $sWeightList, \@arrShipSeparatePackages, \@arrMixedPackages); } ################################################################ # # GetZoneClassCombinations - get the zone class combinations # # Returns: 0 - an array of zone/class array refs defined for the location # # Author: Mike Purnell # ################################################################ sub GetZoneClassCombinations { my @arrZones = GetBands(); my (%hashZones, $nZoneID, $nClassID, @arrZonesClasses); # # Hash the zone IDs for easy checking # foreach $nZoneID (@arrZones) { $hashZones{$nZoneID} = 1; } # # Go through the class hashes in the shipping table checking to # see if one of our zone IDs is defined # foreach $nClassID (keys %ShippingTable) { my $phashClass = $ShippingTable{$nClassID}; # get the class hash foreach $nZoneID (keys %$phashClass) # go through all the zone ID keys { if(defined $hashZones{$nZoneID}) # is this one of our zone IDs? { my @arrClassZone = ($nZoneID, $nClassID); # add the zone/class combination push @arrZonesClasses, \@arrClassZone; } } } return(\@arrZonesClasses); # return our array of array refs } ################################################################ # # AddShippingHash - add a hash reference to our sorted array of # shipping hashes # # This should only be called when @::s_arrSortedShippingHashes # is empty. # # Input: $phashShipping - reference to the shipping hash # # Author: Mike Purnell # ################################################################ sub AddShippingHash { my ($phashShipping) = @_; #? ACTINIC::ASSERT(@::s_arrSortedShippingHashes == 0, 's_arrSortedShippingHashes has entries in it', __LINE__, __FILE__); push @::s_arrSortedShippingHashes, $phashShipping; } ################################################################ # # SetDefaultCharge - Sets the default charge # # Returns: 0 - status - $::SUCCESS if default charge allowed # 1 - error - configuration error message # # Author: Mike Purnell # ################################################################ sub SetDefaultCharge { if($UnknownRegion eq 'Default') # a default charge? { # # Add the default charge hash to our array # AddShippingHash({ 'ShippingLabel' => $$pMessageList[6], 'ShippingClass' => 'Default', 'ShippingZone' => -1, 'Cost' => $UnknownRegionCost, }); return($::SUCCESS, ''); } # # Return an error # return($::FAILURE, $$pMessageList[4]); } ################################################################ # # SetFreeShipping - Sets the free ahipping charge # # Returns: 0 - status - always $::SUCCESS # 1 - error - always '' # # Author: Mike Purnell # ################################################################ sub SetFreeShipping { # # Add the free charge hash to our array # AddShippingHash({ 'ShippingLabel' => $$pMessageList[5], 'ShippingClass' => '-1', 'ShippingZone' => -1, 'Cost' => 0, 'BasisTotal' => GetShippingBasisTotal() }); return($::SUCCESS, ''); } ################################################################ # # SetUndefinedShipping - Sets the shipping undefined # # Returns: 0 - status - always $::SUCCESS # 1 - error - always '' # # Author: Mike Purnell # ################################################################ sub SetUndefinedShipping { # # Add the undefined hash to our array # AddShippingHash({ 'ShippingLabel' => '', 'ShippingClass' => -1, 'ShippingZone' => -1, 'Cost' => 0, }); return($::SUCCESS, ''); } ####################################################### # # OpaqueToHash - populate the hash of the current selection # from the shipping opaque data # # Author: Mike Purnell # ####################################################### sub OpaqueToHash { if(defined $::g_InputHash{ShippingClass}) # if we know the user's selection { $::s_hashShipData{ShippingClass} = $::g_InputHash{ShippingClass}; # just save the class } else # otherwise { %::s_hashShipData = split (';', $::s_Ship_sOpaqueShipData); # restore from opaque data } } ################################################################ # # SaveSelectionToOpaqueData - Save the selected class to the # shipping opaque data # # Input: $parrShipSeparatePackages - reference to array of single item parcels (optional) # $parrMixedPackages - reference to array of mixed item parcels (optional) # # Author: Mike Purnell # ################################################################ sub SaveSelectionToOpaqueData { my($parrShipSeparatePackages, $parrMixedPackages) = @_; # # Simple shipping handles it's own opaque data # if($ShippingBasis eq 'Simple') { return; } # # Check if our current selection is valid # my ($phashShipping, $phashSelected); $phashSelected = undef; foreach $phashShipping (@::s_arrSortedShippingHashes) # for each valid selection { if($$phashShipping{ShippingClass} eq $::s_hashShipData{ShippingClass}) # is this our selected class { $phashSelected = $phashShipping; # save selection last; } } if(!defined $phashSelected && # if we didn't find our selection @::s_arrSortedShippingHashes > 0) # and there are valid options { $phashSelected = $::s_arrSortedShippingHashes[0]; # select the cheapest } if(defined $phashSelected) # if we have a selection { %::s_hashShipData = %$phashSelected; # store to our working hash # # Format the shipping opaque data # $::s_Ship_sOpaqueShipData = sprintf("ShippingClass;%s;ShippingZone;%d;BasisTotal;%s;Cost;%d;", $$phashSelected{ShippingClass}, $$phashSelected{ShippingZone}, $$phashSelected{BasisTotal}, $$phashSelected{Cost}); # # Add the online SSP error handling if present # if(defined $$phashSelected{OnlineError} && $$phashSelected{OnlineError} ne '') { $::s_Ship_sOpaqueShipData .= sprintf('OnlineError;%s;', $$phashSelected{OnlineError}); } # # Add the optimal weight if specified and more than 0 # if($sOptimalWeight ne '' && $sOptimalWeight > 0) { $::s_Ship_sOpaqueShipData .= sprintf('OptimalWeight;%s;', $sOptimalWeight); } # # Set the shipping charge # $::s_Ship_nShipCharges = $$phashSelected{Cost}; # # If this isn't an SSP class, clear the SSP opaque data # if($$phashSelected{ShippingClass} !~ /^\d+_/) { $::s_Ship_sSSPOpaqueShipData = ''; # clear the SSP data } my $sClassID = $$phashSelected{ShippingClass}; # # Add the costs to packaging details # if(defined $parrShipSeparatePackages && defined $parrMixedPackages) { my $phashWeightToCost = (defined $::s_hashClassToWeightCost{$sClassID}) ? $::s_hashClassToWeightCost{$sClassID} : undef; # # Clear our globals # $::s_Ship_sSeparatePackageDetails = ''; $::s_Ship_sMixedPackageDetails = ''; my $parrPackage; foreach $parrPackage (@$parrShipSeparatePackages) { my $sUnitWeight = ($sClassID =~ /^1_/) ? sprintf('%0.03f', int($$parrPackage[2] + 0.9999)) : sprintf('%0.03f', $$parrPackage[2]); my $nUnitCost = (defined $phashWeightToCost) ? $$phashWeightToCost{$sUnitWeight} : 0; $::s_Ship_sSeparatePackageDetails .= sprintf("%s\t%d\t%0.03f\t%d\n", $$parrPackage[0], $$parrPackage[1], $$parrPackage[2], $nUnitCost); } # # The summary record is the last record in the array # my $parrSummary = (@$parrMixedPackages > 0) ? # if we have mixed packages $$parrMixedPackages[-1] : # get the last package undef; # we use this foreach $parrPackage (@$parrMixedPackages) { my $sUnitWeight = ($sClassID =~ /^1_/) ? sprintf('%0.03f', int($$parrPackage[2] + 0.9999)) : sprintf('%0.03f', $$parrPackage[2]); # # Only supply a real unit cost for the summary record # my $nUnitCost = (defined $phashWeightToCost && $parrSummary == $parrPackage) ? $$phashWeightToCost{$sUnitWeight} : 0; $::s_Ship_sMixedPackageDetails .= sprintf("%s\t%d\t%0.03f\t%d\n", $$parrPackage[0], $$parrPackage[1], $$parrPackage[2], $nUnitCost); } } } else { $::s_Ship_sOpaqueShipData = ''; $::s_Ship_nShipCharges = 0; $::s_Ship_sSSPOpaqueShipData = ''; # clear the SSP data } } ################################################################ # # ClearUnusedSSPShippingEntries - Clear any SSP shipping (%::g_ShipInfo) hash entries # # Author: Mike Purnell # ################################################################ sub ClearUnusedSSPShippingEntries { if(CalculateQuantity() == 0) # if we have no items { my $sShipKey; foreach $sShipKey (keys %::g_ShipInfo) # for each entry in the shipping checkout hash { if($sShipKey =~ /^\d+_/) # is this an SSP entry? { delete $::g_ShipInfo{$sShipKey}; # delete it } } return; } } #------------------------------------------------------ # # End of low-level functions # #------------------------------------------------------ #------------------------------------------------------ # # UPS functions # #------------------------------------------------------ ####################################################### # # GetUPSRates - Get the UPS rates # # Input: 0 - the order weight # # Returns: 0 - status code # 1 - error message if any # 2 - ref to an array of class hashes # 3 - rating type (no UPS rate, BasePlusPer rating or UPS rating # ####################################################### sub GetUPSRates { my @arrShippingHashes; my (%hashValidClasses, %hashClassToTotal, $sClassID); # # Clean the SSP entries from the shipping info hash # my $sShipKey; foreach $sShipKey (keys %::g_ShipInfo) # for each entry in the shipping checkout hash { if($sShipKey =~ /^1_/) # is this an SSP entry? { delete $::g_ShipInfo{$sShipKey}; # delete it } } # # Get the setup hash # my $pSSPProvider = GetUPSSetup(); # # Get the merchant and shipment details # my ($nReturnCode, $sError, $sServiceLevelCode, $sRateChart, $sShipperPostalCode, $sShipperCountry, $sConsigneePostalCode, $sConsigneeCountry, $nResidential, $sPackagingType) = GetShipmentDetails(); if($nReturnCode != $::SUCCESS) { return($nReturnCode, $sError); } # # Build the request data to be posted to UPS # my $sRSSRequestDataFormat; $sRSSRequestDataFormat = $::XML_HEADER; $sRSSRequestDataFormat .= GetUPSAccessRequestNode($pSSPProvider); $sRSSRequestDataFormat .= $::XML_HEADER; $sRSSRequestDataFormat .= ""; $sRSSRequestDataFormat .= GetUPSRequestNode('Rate', 'Shop'); $sRSSRequestDataFormat .= ""; $sRSSRequestDataFormat .= " $sRateChart"; $sRSSRequestDataFormat .= ""; $sRSSRequestDataFormat .= ""; $sRSSRequestDataFormat .= " "; $sRSSRequestDataFormat .= "
"; $sRSSRequestDataFormat .= " $sShipperPostalCode"; $sRSSRequestDataFormat .= " $sShipperCountry"; $sRSSRequestDataFormat .= "
"; $sRSSRequestDataFormat .= "
"; $sRSSRequestDataFormat .= " "; $sRSSRequestDataFormat .= "
"; $sRSSRequestDataFormat .= " $sConsigneePostalCode"; $sRSSRequestDataFormat .= " $sConsigneeCountry"; $sRSSRequestDataFormat .= ($nResidential == 1) ? '' : ''; $sRSSRequestDataFormat .= "
"; $sRSSRequestDataFormat .= "
"; $sRSSRequestDataFormat .= " "; $sRSSRequestDataFormat .= " $sServiceLevelCode"; $sRSSRequestDataFormat .= " "; $sRSSRequestDataFormat .= " "; $sRSSRequestDataFormat .= " "; $sRSSRequestDataFormat .= " $sPackagingType"; $sRSSRequestDataFormat .= " "; $sRSSRequestDataFormat .= " "; $sRSSRequestDataFormat .= " %d"; $sRSSRequestDataFormat .= " "; $sRSSRequestDataFormat .= " "; $sRSSRequestDataFormat .= " "; $sRSSRequestDataFormat .= "
"; $sRSSRequestDataFormat .= "
"; # # Split the order into packages of integral weight # my ($phashWeightToQuantity, $parrSortedWeightKeys, $sWeightList) = DivideIntoPackages($::TRUE); # split into packages # # For each weight ask UPS for the rates # my $nWeight; foreach $nWeight (@$parrSortedWeightKeys) # go through our sorted weights { my $sRSSRequestData = sprintf($sRSSRequestDataFormat, $nWeight); # # Get the cost of the shipping for each available classes # my $parrShippingHashes; my $pXmlRoot; ($nReturnCode, $sError, $pXmlRoot) = GetUPSPackageShipping($sRSSRequestData); # Get the UPS response for this rating query if($nReturnCode == $::SUCCESS) # everything OK? { # # Process the list of RatedShipment xml nodes # my $pXmlRatedShipments = $pXmlRoot->GetChildNodes($::UPS_XML_RATED_SHIPMENT); my $pXmlRatedShipment; foreach $pXmlRatedShipment (@{$pXmlRatedShipments}) { # # Get the UPS service level code # my $sServiceCode = $pXmlRatedShipment->GetChildNode($::UPS_XML_SERVICE)->GetChildNode($::UPS_XML_SERVICE_CODE)->GetNodeValue(); my $sClassID = "1_$sServiceCode"; # # Check that this service is acceptable to the merchant # if(defined $$pSSPProvider{ServiceLevelCode}{$sServiceCode}) { # # Get the cost of shipping a package with the specified weight # my $pXmlTotalCharges = $pXmlRatedShipment->GetChildNode($::UPS_XML_TOTAL_CHARGES); my $sCurrencyCode = $pXmlTotalCharges->GetChildNode($::UPS_XML_CURRENCY_CODE)->GetNodeValue(); my $sMonetaryValue = $pXmlTotalCharges->GetChildNode($::UPS_XML_MONETARY_VALUE)->GetNodeValue(); my $nIntegralCost = int($sMonetaryValue * 100 + 0.999); # # Add the cost to our totals hash # $hashClassToTotal{$sClassID} += $$phashWeightToQuantity{$nWeight} * $nIntegralCost; # add quantity * cost to total # # If this is the first time we've seen this class, save the hash reference # if(!defined $hashValidClasses{$sClassID}) { $hashValidClasses{$sClassID} = { 'ShippingLabel' => GetUPSServiceName($sServiceCode), 'ShippingClass' => $sClassID, 'ShippingZone' => -1, }; } # # Add to the class to weight/cost hash # $::s_hashClassToWeightCost{$sClassID}{sprintf('%0.03f', $nWeight)} = $nIntegralCost; } } } elsif ($nReturnCode == $::FAILURE) # e.g. server unavailable error { return(HandleUPSOnlineError($sError, $parrSortedWeightKeys, $phashWeightToQuantity, $sWeightList)); } else # Bad data - e.g. oversized packages { # # Return an empty shipping hash and Shipping Band Not Defined logic will determine the following actions # @arrShippingHashes = (); return($::SUCCESS, '', \@arrShippingHashes, $::UPS_CLASSES_NOT_USED); } } # # Now populate the array of hashes of to return # my $nRatingType = $::UPS_CLASSES_NOT_USED; foreach $sClassID (keys %hashValidClasses) # for each valid class { my $phashShipping = $hashValidClasses{$sClassID}; # get a shipping hash $$phashShipping{BasisTotal} = GetShippingBasisTotal(); # set the weight $$phashShipping{Cost} = $hashClassToTotal{$sClassID}; # adjust the cost push @arrShippingHashes, $phashShipping; # add to the array my $dUPSCost = $hashClassToTotal{$sClassID} / 100; # # Save the pseudo UPS raw response to the shipping info # $::g_ShipInfo{$sClassID} = "UPSOnLine%1.2\%0000%0000Success%4%$sServiceLevelCode%$sShipperPostalCode%US%$sConsigneePostalCode%$sConsigneeCountry%000%1%$dUPSCost%0.00%$dUPSCost%-1"; $nRatingType = $::UPS_CLASSES_USED; } return($::SUCCESS, '', \@arrShippingHashes, $nRatingType); } ####################################################### # # GetUPSPackageShipping - get the classes and costs for # a package of a given weight # # Input: $sRequestData - the request data # $nWeight - weight of the package # # Returns: 0 - status code # 1 - error message if any # 2 - ref to array of shipping hashes # ####################################################### sub GetUPSPackageShipping { my ($sRequestData, $nWeight) = @_; my (@arrShippingHashes); # # Set the maximum number of retries before fallback # my $nRetries = 2; # # Testing code to simulate a UPS failure # # if($::s_sDeliveryCountryCode eq 'US' && # ActinicLocations::GetISODeliveryRegionCode() eq 'NOUPS') # { # $nRetries = 0; # } return(UPS_SendAndReceive('/ups.app/xml/Rate', $sRequestData, $nRetries, 2253)); } ####################################################### # # HandleUPSOnlineError - Handle an online error # # Input: $sResponse - reason for failure # $parrSortedWeightKeys - an array of sorted weight keys # $phashWeightToQuantity - hash of weights to quantities # $sWeightList - csv list of qty@weight # # Returns: 0 - status code # 1 - error message if any # 2 - ref to array of shipping hashes # 3 - rating type (BasePlusPer rating or no rating) # ####################################################### sub HandleUPSOnlineError { my ($sResponse, $parrSortedWeightKeys, $phashWeightToQuantity, $sWeightList) = @_; my ( $sRateChart, $sShipperPostalCode, $sConsigneePostalCode, $sConsigneeCountry, $sPackagingType); my (@arrShippingHashes); # # Get the UPS hash ref # my $pSSPProvider = GetUPSSetup(); # # Rating type = no rating until we don't add any classes # my $nRatingType = $::UPS_CLASSES_NOT_USED; $sRateChart = $$pSSPProvider{'RateChart'}; $sShipperPostalCode = $$pSSPProvider{'ShipperPostalCode'}; $sPackagingType = $$pSSPProvider{'PackagingType'}; $sConsigneePostalCode = $::g_ShipContact{'POSTALCODE'}; # # Get the consignee ISO country code # $sConsigneeCountry = ActinicLocations::GetISODeliveryCountryCode(); # # log the error so we know fallback plan used # my $sErrorText = ACTINIC::GetPhrase(-1, 2292, $sResponse); ACTINIC::RecordErrors($sErrorText, ACTINIC::GetPath()); # # checked on any error # if($$::g_pSSPSetupBlob{NotifyMerchantOfFailure}) { my ($Status, $Message) = ACTINIC::SendMail($::g_sSmtpServer, $$::g_pSSPSetupBlob{FailureEmailAddress}, ACTINIC::GetPhrase(-1, 2291), $sErrorText, $$::g_pSSPSetupBlob{FailureEmailAddress}); # # just record the error if mail problem # if ($Status != $::SUCCESS) { ACTINIC::RecordErrors("$sErrorText:\n sending$Message" , ACTINIC::GetPath()); } } # # now decide which classes to put into the shipping dropdown # # virtual zero-cost class # if($$::g_pSSPSetupBlob{ConfirmShippingByEmail}) { # # Save the UPS raw response to the shipping info # $::g_ShipInfo{"1_$sCONFIRM_BY_EMAIL"} = "UPSOnLine%1.2\%0000%0000Success%4%000%$sShipperPostalCode%US%$sConsigneePostalCode%$sConsigneeCountry%000%1%0.00%0.00%0.00%-1"; # # Save the error handling # $sOnlineError = 'Email'; push @arrShippingHashes, { 'ShippingLabel' => GetUPSServiceName($sCONFIRM_BY_EMAIL), 'ShippingClass' => "1_$sCONFIRM_BY_EMAIL", 'ShippingZone' => -1, 'Cost' => 0, 'BasisTotal' => GetShippingBasisTotal(), 'OnlineError' => 'Email' }; # # Don't update %::s_hashClassToWeightCost as we don't know the package costs # at this time # } # # Fallback "Base plus Per" table computation # elsif($$::g_pSSPSetupBlob{UseClassDefaultFormula}) { # # Save the error handling # $sOnlineError = 'BasePlusIncrement'; my @arrServiceLevelCodes; if($::s_sDeliveryCountryCode eq 'CA') { push @arrServiceLevelCodes, '11', '07', '08'; #'STD', 'XPR', 'XPD' } elsif($::s_sDeliveryCountryCode eq 'US') { push @arrServiceLevelCodes, '14', '01', '13', '59', '02', '12', '03'; #'1DM', '1DA', '1DAPI'(missing), '1DP', '2DM', '2DA', '3DS', 'GND' } else { push @arrServiceLevelCodes, '07', '08'; #'XPR', 'XPD' } my $sServiceLevelCode; foreach $sServiceLevelCode (@arrServiceLevelCodes) { # # Check if the merchant accepts this service # if(defined $$pSSPProvider{'ServiceLevelCode'}{$sServiceLevelCode}) { my ($nWeight, $nTotalCost); foreach $nWeight (@$parrSortedWeightKeys) { # # Calculate the incremental units # my $nIncrementalUnits = int(($nWeight / $$pSSPProvider{'ServiceLevelCode'}{$sServiceLevelCode}[3]) + 0.999); # # Calculate the fallback cost as base plus charge * increments # my $nIntegralCost = $$pSSPProvider{'ServiceLevelCode'}{$sServiceLevelCode}[1] + ($$pSSPProvider{'ServiceLevelCode'}{$sServiceLevelCode}[2] * $nIncrementalUnits); # # Add package cost * quantity to our total cost # $nTotalCost += $$phashWeightToQuantity{$nWeight} * $nIntegralCost; # # Convert to UPS format # my $dUPSCost = $nIntegralCost / 100; # # Save the pseudo UPS raw response to the shipping info # $::g_ShipInfo{"1_$sServiceLevelCode" . "_$nWeight"} = "UPSOnLine%1.2\%0000%0000Success%4%$sServiceLevelCode%$sShipperPostalCode%US%$sConsigneePostalCode%$sConsigneeCountry%000%1%$dUPSCost%0.00%$dUPSCost%-1"; # # Add to the class to weight/cost hash # $::s_hashClassToWeightCost{"1_$sServiceLevelCode"}{sprintf('%0.03f', $nWeight)} = $nIntegralCost; } # # Convert to UPS format # my $dUPSCost = $nTotalCost / 100; # # Save the pseudo UPS raw response to the shipping info # $::g_ShipInfo{"1_$sServiceLevelCode"} = "UPSOnLine%1.2\%0000%0000Success%4%$sServiceLevelCode%$sShipperPostalCode%US%$sConsigneePostalCode%$sConsigneeCountry%000%1%$dUPSCost%0.00%$dUPSCost%-1"; push @arrShippingHashes, { 'ShippingLabel' => GetUPSServiceName($sServiceLevelCode), 'ShippingClass' => "1_$sServiceLevelCode", 'ShippingZone' => -1, 'Cost' => $nTotalCost, 'BasisTotal' => GetShippingBasisTotal(), 'OnlineError' => 'BasePlusIncrement' }; $nRatingType = $::UPS_BASEPLUSPER_CLASSES_USED; } } } # # return reference to the shipping hashes # return($::SUCCESS, '', \@arrShippingHashes, $nRatingType); } ####################################################### # # GetShipmentDetails - Get the request data for UPS RSS # # Returns: 0 - Return Code, # 1 - error message, # 2 - $sServiceLevelCode, # 3 - $sRateChart, # 4 - $sShipperPostalCode, # 5 - $sConsigneePostalCode, # 6 - $sConsigneeCountry, # 7 - $nResidential, # 8 - $sPackagingType) # ####################################################### sub GetShipmentDetails { my ($nReturnCode, $sError, $sServiceLevelCode, $sRateChart, $sShipperPostalCode, $sShipperCountry, $sConsigneePostalCode, $sConsigneeCountry, $nResidential, $sPackagingType); # # Get the UPS hash ref # my $pSSPProvider = GetUPSSetup(); $sRateChart = $$pSSPProvider{'RateChart'}; $sShipperPostalCode = $$pSSPProvider{'ShipperPostalCode'}; $sShipperCountry = $$pSSPProvider{'ShipperCountry'}; $sPackagingType = $$pSSPProvider{'PackagingType'}; $sConsigneePostalCode = $::g_ShipContact{'POSTALCODE'}; # # Get the consignee ISO country code # $sConsigneeCountry = ActinicLocations::GetISODeliveryCountryCode(); if($sConsigneeCountry eq 'CA') { if($sConsigneePostalCode !~ /^(\w\d\w) (\d\w\d)$/) { # # warn buyer about invalid Canadian postcode format # return($::FAILURE, ACTINIC::GetPhrase(-1, 2149)); } $sConsigneePostalCode =~ s/\s*//g; $sServiceLevelCode = '11'; } elsif($sConsigneeCountry eq 'US') { # # Check the US zip code is in sensible format # my ($nStatus, $sError); ($nStatus, $sError, $sConsigneePostalCode) = GetUS5DigitZipCode($sConsigneePostalCode); if($nStatus == $::FAILURE) { return($nStatus, $sError); } # # On Call Air Pickup and Letter Center is only available for air shipments within US # if($sRateChart eq '07' or $sRateChart eq '19') { $sServiceLevelCode = '02'; } else { $sServiceLevelCode = '03'; } } else { $sServiceLevelCode = '07'; } # # Set the residential flag # $nResidential = $::g_LocationInfo{DELIVERRESIDENTIAL} ne '' ? 1 : 0; return($::SUCCESS, '', $sServiceLevelCode, $sRateChart, $sShipperPostalCode, $sShipperCountry, $sConsigneePostalCode, $sConsigneeCountry, $nResidential, $sPackagingType); } ####################################################### # # GetUPSSetup - Return a reference to the UPS setup # # Returns: 0 - reference to the UPS setup hash # ####################################################### sub GetUPSSetup { return($$::g_pSSPSetupBlob{1}); } ####################################################### # # GetUPSServiceName - Return a UPS Service name # # Returns: 0 - reference to the UPS setup hash # ####################################################### sub GetUPSServiceName { my ($sServiceLevelCode) = @_; if($sServiceLevelCode eq $sCONFIRM_BY_EMAIL) { return(ACTINIC::GetPhrase(-1, 2100)); } return($$::g_pSSPSetupBlob{1}{ServiceLevelCode}{$sServiceLevelCode}[0]); } ####################################################### # # CheckUPSAddressValidation - Check the address with UPS # # Any technical failures are ignored, the only errors are invalid # zip code (internal) or mismatched state/cipy/zip (our message but # error is raised by UPS) # # Input: $sConsigneeCountry - delivery country code # $sConsigneeState - delivery ISO state code # $sConsigneeCity - delivery city # $sConsigneePostalCode - delivery zip code # Returns: 1 - result code # 0 - error message if any # ####################################################### sub DoUPSAddressValidation { my ($sConsigneeCountry, $sConsigneeState, $sConsigneeCity, $sConsigneePostalCode) = @_; # # If the country is unknown or not the US # if($sConsigneeCountry ne 'US' || $sConsigneeCountry eq '' || $sConsigneeCountry eq '---') { return($::SUCCESS, ''); } # # Get the UPS hash # my $pSSPProvider = GetUPSSetup(); # # Filter out the non-mainland and Hawaii states # # # Set up an array of UPS-acceptable states # my @arrStates = ( 'AK', 'AL', 'AR', 'AZ', 'CA', 'CO', 'CT', 'DC', 'DE', 'FL', 'GA', 'HI', 'IA', 'ID', 'IL', 'IN', 'KS', 'KY', 'LA', 'MA', 'MD', 'ME', 'MI', 'MN', 'MO', 'MS', 'MT', 'NC', 'ND', 'NE', 'NH', 'NJ', 'NM', 'NV', 'NY', 'OH', 'OK', 'OR', 'PA', 'RI', 'SC', 'SD', 'TN', 'TX', 'UT', 'VA', 'VT', 'WA', 'WI', 'WV', 'WY', ); my $sStatesString = join('|', @arrStates); if($sStatesString !~ $sConsigneeState) { # # Allow our UPS test state to by-pass the 51 states test # # if($sConsigneeState ne 'NOUPS') { # # Only display an error message if we have online rates enabled # if(defined $$pSSPProvider{'RSSEnabled'} && $$pSSPProvider{'RSSEnabled'}) { # # Inform the merchant about the mis-configuration # my $sErrorText = sprintf(ACTINIC::GetPhrase(-1, 2099), ACTINIC::GetCountryName("US.$sConsigneeState")); return($::FAILURE, $sErrorText); } return($::SUCCESS, ''); # not supported so no error } } # # Check the US zip code is in sensible format otherwise tell them off # my ($nStatus, $sError); ($nStatus, $sError, $sConsigneePostalCode) = GetUS5DigitZipCode($sConsigneePostalCode); if($nStatus == $::FAILURE) { return($::FAILURE, $sError); } my (@Response); # # Start building the UPS request # my $pSSPProvider = GetUPSSetup(); my $sAVRequestData = ''; # # Construct header data # $sAVRequestData = $::XML_HEADER; $sAVRequestData .= GetUPSAccessRequestNode($pSSPProvider); # # Construct the request # $sAVRequestData .= $::XML_HEADER; $sAVRequestData .= ""; $sAVRequestData .= GetUPSRequestNode('AV'); # # Construct address information # $sAVRequestData .= "
"; # # Add the state if known # if($sConsigneeState ne '') { # # Strip off the country portion # $sConsigneeState =~ s/^\w\w\.//; $sAVRequestData .= "$sConsigneeState"; } # # Add the city if known # if($sConsigneeCity ne '') { $sAVRequestData .= "$sConsigneeCity"; } # # Add the zip code if known # if($sConsigneePostalCode ne '') { $sAVRequestData .= "$sConsigneePostalCode"; } $sAVRequestData .= "
"; $sAVRequestData .= "
"; # # Set the maximum number of retries before fallback # my $nRetries = 2; # # Testing code to simulate a UPS failure # # if($sConsigneeState eq 'NOUPS') # { # $nRetries = 0; # } # # Try and connect to the UPS site # my ($Result, $sMessage, $pXmlRoot) = UPS_SendAndReceive('/ups.app/xml/AV', $sAVRequestData, $nRetries, 2305); if ($Result != $::SUCCESS) { return ($Result, $sMessage); } # # Process the response list # my $bValidationFailed = $::TRUE; my $raAddressValidationResults = $pXmlRoot->GetChildNodes($::UPS_XML_ADDRESS_VALIDATION_RESULT); my $pXmlAddressValidationResult; foreach $pXmlAddressValidationResult (@{$raAddressValidationResults}) { # # Get the UPS Rank # my $sRank = $pXmlAddressValidationResult->GetChildNode($::UPS_XML_RANK)->GetNodeValue(); # # Get the UPS Quality # my $sQuality = $pXmlAddressValidationResult->GetChildNode($::UPS_XML_QUALITY)->GetNodeValue(); # # Get the UPS State # my $sState = $pXmlAddressValidationResult->GetChildNode($::UPS_XML_ADDRESS)->GetChildNode($::UPS_XML_STATE_PROVINCE_CODE)->GetNodeValue(); # # Get the UPS City # my $sCity = $pXmlAddressValidationResult->GetChildNode($::UPS_XML_ADDRESS)->GetChildNode($::UPS_XML_CITY)->GetNodeValue(); # # Get the UPS PostalCodeLow # my $sPostalCodeLow = $pXmlAddressValidationResult->GetChildNode($::UPS_XML_POSTAL_CODE_LOW_END)->GetNodeValue(); # # Get the UPS PostalCodeHigh # my $sPostalCodeHigh = $pXmlAddressValidationResult->GetChildNode($::UPS_XML_POSTAL_CODE_HIGH_END)->GetNodeValue(); # # If we find a suitable address then the user-specified address is valid # if($sState eq $sConsigneeState && (defined $::g_InputHash{LocationDeliveryCountry} || (lc($sCity) eq lc($sConsigneeCity) || $sConsigneeCity eq '')) && ($sConsigneePostalCode eq $sPostalCodeLow || ($sConsigneePostalCode gt $sPostalCodeLow && $sPostalCodeHigh ne '' && $sConsigneePostalCode le $sPostalCodeHigh))) { $bValidationFailed = $::FALSE; } } # # If we got a valid response from UPS, but the validation failed # report the error # if($bValidationFailed) { my $sErrorText = ACTINIC::GetPhrase(-1, 2305, ACTINIC::GetPhrase(-1, 2072)); return($::BADDATA, $sErrorText); } return($::SUCCESS, ''); } ####################################################### # # UPS_SendAndReceive - send some data to the UPS server # and do some error handling on the response # # Params: 0 - the path on the UPS server # 1 - the message to be sent # 2 - max. number of attempts to connect # 3 - error phrase ID for response error display # # Returns: 0 - result # 1 - message # 2 - reference to the UPS response xml node # # Author: Tibor Vajda # ####################################################### sub UPS_SendAndReceive { #? ACTINIC::ASSERT($#_ == 3, "Invalid argument count in UPS_SendAndReceive ($#_)", __LINE__, __FILE__); # # Grab parameters # my ($sPath, $sRequestData, $nRetries, $nErrorTitlePhrase) = @_; # # Try and connect to the UPS site # my (@Response, $sHTTPResponse, $sHTTPHeader, $sHTTPContent, $phashHeader); while($nRetries && $bUPS_Available) # UPS was available at the previous function call { @Response = ACTINIC::HTTPS_SendAndReceive('www.ups.com', 443, $sPath, $sRequestData, 'POST', $::FALSE, $ssl_socket); if($Response[0] != $::SUCCESS || $Response[2] eq '') { $nRetries--; # one less time } else # success { $sHTTPResponse = $Response[2]; $ssl_socket = $Response[3]; last; # leave the retry loop } } unless ($sHTTPResponse) { $bUPS_Available = $::FALSE; # to remember that the server is unavailable return($::FAILURE, $Response[1]); } # # Split the HTTP response up # @Response = ACTINIC::HTTP_SplitHeaderAndContent($sHTTPResponse); if($Response[0] != $::TRUE) { return($::FAILURE, $Response[1]); } # # Store the successful response # $sHTTPHeader = $Response[2]; $sHTTPContent = $Response[3]; $phashHeader = $Response[4]; # # Check we have a content type # my $sContentType = $$phashHeader{'Content-Type'}; unless($sContentType) { return($::FAILURE, ACTINIC::GetPhrase(-1, 2293)); } # # Ignore text/html for Datastream messages # if($sContentType =~ /application\/xml/) { my $pParser = new PXML(); my ($sParsedText, $pXmlRoot) = $pParser->Parse($sHTTPContent); $pXmlRoot = $pXmlRoot->[0]; # # Check for errors # my ($Result, $sMessage) = ParseUPSResponseNode($pXmlRoot->GetChildNode($::UPS_XML_RESPONSE), $nErrorTitlePhrase); return($Result, $sMessage, $pXmlRoot); } return($::FAILURE, ACTINIC::GetPhrase(-1, 2293)); } ####################################################### # # GetUPSAccessRequestNode - Construct the access request xml node # # Params: 0 - SSP provider blob hash # # Returns: 0 - access request xml node text # # Author: Tibor Vajda # ####################################################### sub GetUPSAccessRequestNode { my ($pSSPProvider) = @_; my $sAccessKey = ACTINIC::DecodeXOREncryption($$pSSPProvider{AccessKey}, $::UPS_ENCRYPT_PASSWORD); my $sUserName = ACTINIC::DecodeXOREncryption($$pSSPProvider{UserName}, $::UPS_ENCRYPT_PASSWORD); my $sPassword = ACTINIC::DecodeXOREncryption($$pSSPProvider{Password}, $::UPS_ENCRYPT_PASSWORD); my $sAccessRequestNode = ''; $sAccessRequestNode .= ""; $sAccessRequestNode .= "$sAccessKey"; $sAccessRequestNode .= "$sUserName"; $sAccessRequestNode .= "$sPassword"; $sAccessRequestNode .= ""; return $sAccessRequestNode; } ####################################################### # # GetUPSAccessRequestNode - Construct the access request xml node # # Params: 0 - SSP provider blob hash # # Returns: 0 - access request xml node text # # Author: Tibor Vajda # ####################################################### sub GetUPSRequestNode { my ($sAction, $sOption) = @_; my $sRequestNode = ''; $sRequestNode .= ""; $sRequestNode .= ""; $sRequestNode .= "$::UPS_XPCI_VERSION"; $sRequestNode .= ""; $sRequestNode .= "$sAction"; if (defined $sOption) { $sRequestNode .= "$sOption"; } $sRequestNode .= ""; return $sRequestNode; } ####################################################### # # ParseUPSResponseNode - Check e.g. if response has error # # Params: 0 - UPS response xml node # # Returns: 0 - result # $::SUCCESS if OK # $::BADDATA if the response has a # 1 - error string # # Author: Tibor Vajda # ####################################################### sub ParseUPSResponseNode { my ($pXmlResponse, $nErrorTitlePhrase) = @_; my $pXmlStatusCode = $pXmlResponse->GetChildNode($::UPS_XML_RESPONSE_STATUS_CODE); if (!defined($pXmlStatusCode)) { return ($::FAILURE, ACTINIC::GetPhrase(-1, $nErrorTitlePhrase, ACTINIC::GetPhrase(-1, 2294))); # malformed xml error } # # Check if everything went fine # if ($pXmlStatusCode->GetNodeValue() eq $::UPS_SUCCESSFUL) { return($::SUCCESS, '') } # # There was some problem - look at them # my $paXmlErrors = $pXmlResponse->GetChildNodes($::UPS_XML_ERROR); my $pXmlError; foreach $pXmlError (@$paXmlErrors) { # # Extract severity # my $pXmlErrorSeverity = $pXmlError->GetChildNode($::UPS_XML_ERROR_SEVERITY); if (!defined($pXmlErrorSeverity)) { return ($::FAILURE, ACTINIC::GetPhrase(-1, $nErrorTitlePhrase, ACTINIC::GetPhrase(-1, 2294))); # malformed xml error } my $sSeverity = $pXmlErrorSeverity->GetNodeValue(); # # Extract description # my $pXmlErrorDescription = $pXmlError->GetChildNode($::UPS_XML_ERROR_DESCRIPTION); if (!defined($pXmlErrorDescription)) { return ($::FAILURE, ACTINIC::GetPhrase(-1, $nErrorTitlePhrase, ACTINIC::GetPhrase(-1, 2294))); # malformed xml error } my $sErrorDescription = $pXmlErrorDescription->GetNodeValue(); # # Handle different kind of severities # if ($sSeverity eq $::UPS_ERROR_SEVERITY_HARD_ERROR) # hard error probably due to the info provided { return ($::BADDATA, ACTINIC::GetPhrase(-1, $nErrorTitlePhrase, $sErrorDescription)); } elsif ($sSeverity eq $::UPS_ERROR_SEVERITY_TRANSIENT_ERROR) # temporary server problem - failure and not bad data { return ($::FAILURE, ACTINIC::GetPhrase(-1, $nErrorTitlePhrase, $sErrorDescription)); } elsif ($sSeverity eq $::UPS_ERROR_SEVERITY_WARNING) # not an error { # # We treat it as success ATM # } else { return ($::FAILURE, ACTINIC::GetPhrase(-1, $nErrorTitlePhrase, ACTINIC::GetPhrase(-1, 2294))); # malformed xml error } } # # There were no serious errors, so return success # return($::SUCCESS, '') } #------------------------------------------------------ # # End of UPS functions # #------------------------------------------------------ return ($::SUCCESS); # # End of ShippingTemplate.pl #