8915413;
#---------------------------------------------------------------
#
# Generated by ISControlISConfigCtrl
#
#---------------------------------------------------------------
#
my $bCompleteOnTechnicalFailures = $::FALSE;
my $bCompleteOnFinancialFailures = $::FALSE;
my $bAuthorize = $::TRUE;
my $bTestMode = $::FALSE;
my $sGTime = '1779794149';
my $sSanity = '957e0fe3b7e319e76a5c547728279407';
my $sProcessScriptURL = 'https://api.paypal.com';
my $sADF01 = 'AQXv1AkhrXCKeHmhhzdEDI2fCb9uulVFsIDeIt4YEcDthf5kDfpyccbFfdt5qhzLrenAaK5HqHk3Eq5A';
my $sADF02 = '80 89a3214d0b6acb53bb29c341a6466632278ac8dfcb0ae32af0a8edc0332ac36cc59eb4802f7ff3f06ae39cd187f0e8c14d86fe9c67c2d47ed381109a22803b6d3a4f20642ab45fc3445eb7045eac8816';
my $sADF03 = '17 f1156e347736c08fb50e952826ad5a1f6700c035f5843b5b';
my $sADF04 = '0';
my $sADF05 = '';
my $sADF06 = '1';
my $sADF07 = '0';
my $sCountriesRequiringStateCodes = 'US';
my $sPARTNERID = '20 d0cc4fd32843d406e7b7ae2fe9b78f3c9bfa771fb62ca9a3';
my $sADF08 = 'FALSE';
my $sADFDump = '02010280000001A800000000D705C9E4FDB3B06A3C9E343B8914D6AF754C6E333CCC9B74EC042C24A6228D951590ABAC4A75D56A977AB48E573EB9293F75EDECC4EA1F7E2A25F555CC2DF52E26949E668985E21F2F810551A573BEEFA14F08A04972C74FCC609C5DC9D92382F147B93DED2B23944F6C84CE91E680102ACD20E628B0C4666C060BC0895A9D11EA176C68B028987B8D6E74D1B2AEDA1A91233E7A1B533758FC8F3E6CC58E8FF3FB825E8F489BD82A08C3C174524E96284810EB4D9692FFEC6C9F952AFCEE99500C36223AFD9347622B2FE0BA12D5FA64D29B6088F93EA7CB99F95BF3E4A744F45DC9F151A411CEBFEFEE223F6A68BCA735F2CFC0F987059853761F19E5690041D489F4E22135076D543EBB6DEE73CBF8A0F5D5422715184DEEF58F7640C63F7DC6AF75AE061B6FF75E410AD95ED58871D8C0476F41846798BA67BEFF048C11A51662F6E2932EBF9185219F5F9C5038E37F3683921B74BC02631FAAECC0D705807D87F76D327FF367D7C33969DE75B0257EEC522177411329376291723B9668269993DF21BDC9EF48DC18F3CB446EA74E5C04FAF260402EEC80CFAE3BB2D05D1CE21FECAE3A0B2BE54A9A0E5A1C4654BEF1D54236FFB427DAA8F8C82B0BC8C704FFC4F3E5D9E7907A2E8CD70C525C846BA87381DB6002EA2AEA0F2317E392721C49F0C7A7629D958C11C900A6A27A2DD419725C291F6322B44775854B9AF8212378B2B36EF8D4428C3CDDFEB4B14FDF24662C2F0183390D668FD678EA734101914075C00DF3BCD064';
#
#---------------------------------------------------------------
#
# OCCPayPal.pl	- PayPal Chekout OCC script
#
# Copyright (c) Sellerdeck Limited 2020 All rights reserved
#
# *** Do not change this code unless you know what you are doing ***
#
# This script is called by an eval() function
#
#  $Revision: 38216 $
#
#---------------------------------------------------------------

use strict;

sub GetPspPage1		{	return (PayPal::GetPspPage1()			);	}
sub GetPspPage2		{	return (PayPal::GetPspPage2()			);	}
sub GetPspPage3		{	return (PayPal::GetPspPage3()			);	}
sub GetPspCancelPage	{	return ($::FAILURE, "Unsupported method (GetPspCancelPage)");	}
sub ProcessIPN			{	return (PayPal::ProcessWebhook()		); }

package PayPal;
#
# Parameters depend on PSP
# Common parameters
#
$PayPal::PSP_PARTNERID = ACTINIC::DecryptPspParam($sPARTNERID);
$PayPal::PSP_CLIENT_ID = $sADF01;
$PayPal::PSP_SECRET_KEY = ACTINIC::DecryptPspParam($sADF02);
#
# PayPal (500)
#
$PayPal::PSP_WEBHOOKID = ACTINIC::DecryptPspParam($sADF03);
$PayPal::PSP_LANDING_PAGE = $sADF04;
#
# Suspend variable checks for the following as 
# they are not present for PayPal Cards (501)
#
no strict 'vars';
$PayPal::PSP_BRAND_NAME = $sADF05;
$PayPal::PSP_CREDIT_ALLOWED = $sADF06 == 1 ? $::FALSE : $::TRUE;
$PayPal::PSP_APMS_ALLOWED = $sADF07 == 1 ? $::FALSE : $::TRUE;
$PayPal::PSP_APMS_SYNCHRONOUS = $sADF07 == 2 ? $::TRUE : $::FALSE;
use strict;
#
# PayPal Cards (501)
#
$PayPal::PSP_3DS_ACCEPT_ENROLLED = $sADF03 == 1 ? $::TRUE : $::FALSE;
$PayPal::PSP_3DS_ACCEPT_UNENROLLED = $sADF04 == 1 ? $::TRUE : $::FALSE;
#
# Until PayPal provide a mechanism to disable all APMs it
# is necessary to maintain the list of supported APMs here.
# If PayPal add support for a new APM it will automatically
# be enabled even if the merchant has chosen not to allow
# APMs until the new APM is added to this list
#
@PayPal::APM_FUNDING_LIST = ('venmo,sepa,bancontact,eps,giropay,ideal,mybank,p24,sofort');

$PayPal::HTTP_TIMEOUT = 10;
$PayPal::HTTP_MAX_RETRIES = 3;
$PayPal::HTTP_RETRY_INTERVAL = 2;					# seconds before retry

$PayPal::PORT = 443;
$sProcessScriptURL .= "/";
$sProcessScriptURL =~ /https?:\/\/(.*?):?\d*(\/.*)$/;
$PayPal::PROCESS_SCRIPT_HOST = $1;

$PayPal::INTENT = $bAuthorize ? 'CAPTURE' : 'AUTHORIZE';
$PayPal::PAYEE_PREFERRED = $bAuthorize || $PayPal::PSP_APMS_SYNCHRONOUS ? 'IMMEDIATE_PAYMENT_REQUIRED' : 'UNRESTRICTED';
$PayPal::PSP_API_VERSION = 2;
$PayPal::PSP_WEBHOOK_VERSION = 1;

$PayPal::IPN = $::FALSE;								# assume not an IPN (webhook)
$PayPal::WEBHOOKMESSAGEID = '';

$PayPal::CONTACTMSG = ACTINIC::GetPhrase(-1, 1964);
$PayPal::RETRYMSG = " Please try again or choose a different payment method.";
$PayPal::PAYPAL_ERROR = 'A problem occurred while processing your payment request.' . $PayPal::RETRYMSG;
$PayPal::REAUTHORISE_MESSAGE_FORMAT = "We’re sorry but we were not able to process your payment as the final amount %s differs too greatly from the initially approved amount %s within PayPal.</br>To finalise your order, please click on the PayPal button below.";

$PayPal::TESTMODE	= $bTestMode;
if ($bTestMode)
	{
	$PayPal::TESTMESSAGE = 'PayPal is running in Test mode</br>';	 # warn we are in test mode
	}
else
	{
	$PayPal::TESTMESSAGE = '';
	}

#######################################################
#
# GetPspPage1 - PayPal page 1 gets a PayPal Order ID
#
#	Requires: 	$::g_InputHash
#
#	Returns:	0 - status $::SUCCESS or $::FAILURE
#				1 - error message if status not $::SUCCESS
#
#######################################################

sub GetPspPage1
	{
	LogData("PayPal::GetPspPage1: Started");
	my ($nStatus, $sError, $hOrderDetails);
	#
	# Deterimine if being called from payment page or not
	#
	my $bPaymentPage = ($::g_InputHash{'PAGE'} eq 'PAYMENT') ? $::TRUE : $::FALSE;
	#
	# Record the current order total in internal (pence/cents) format
	#
	$::g_PaymentInfo{'ORDERTOTAL'} = ActinicOrder::GetOrderTotal($::TRUE);
	#
	# On the payment page we need to validate shipping and tax as
	# we will be creating the sales order after the PayPal order
	#
	if ($bPaymentPage)
		{
		#
		# Validate tax and shipping info`
		#
		($nStatus, $sError) = ValidateShippingAndTax();
		if ($nStatus != $::SUCCESS)
			{
			return ($::FAILURE, $sError);
			}
		#
		# Create the Sellerdek Order (allocates the order number)
		#
		($nStatus, $sError) = main::CompleteOrder();
		if ($nStatus != $::SUCCESS)
			{
			#
			# Cannot complete the Sales Order
			#
			RecordErrors(sprintf("PayPal::GetPspPage1: Complete Order returned [%s]", $sError));
			return ($::FAILURE, $sError);
			}
		}

	($nStatus, $sError, $hOrderDetails) = CreatePayPalOrder($bPaymentPage);
	if ($nStatus != $::SUCCESS)
		{
		#
		# Cannot create a new Order
		#
		RecordErrors(sprintf("PayPal::GetPspPage1: Cannot create a PayPal Order [%s]", $sError));
		return ($::FAILURE, $PayPal::PAYPAL_ERROR);
		}

	my $sOrderId = $hOrderDetails->{'id'};
	LogData("PayPal::GetPspPage1: Created PayPal Order $sOrderId");
	#
	# All done, save the session and send JSON response
	#
	my $hResponse = {'orderID' => $sOrderId};
	$::Session->SetProviderParam($::PAYMENT_PAYPALCHECKOUT, $::PAYPAL_ORDER_ID, $sOrderId);
	SaveSessionAndSendResponse($hResponse);

	LogData("PayPal::GetPspPage1: Finished successfully");

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

#######################################################
#
# GetPspPage2 - PayPal page 2 validates the order and
#					 displays the shipping confirmation page
#					 but only if not Custom Card Fieds on
#					 the payment page
#
#	Requires: 	$::g_InputHash
#
#	Returns:	0 - status $::SUCCESS or $::FAILURE
#				1 - error message if status not $::SUCCESS
#				2 - HTML of page IF error
#
#######################################################

sub GetPspPage2
	{
	#
	# Get our PayPal order ID
	#
	LogData("PayPal::GetPspPage2: Started");
	my $sOrderId = $::Session->GetProviderParam($::PAYMENT_PAYPALCHECKOUT, $::PAYPAL_ORDER_ID);
	if ($::g_InputHash{'ORDERID'} ne $sOrderId)
		{
		ACTINIC::ReportError(sprintf("PayPal::GetPspPage2: unexpected order ID %s. Expected %s", $::g_InputHash{'ORDERID'}, $sOrderId));
		}
	#
	# Processing for requests from the Payment Page
	# is different to requests from the Cart Page
	#
	if (($::g_InputHash{'PAGE'} eq 'PAYMENT') &&
		 ($::g_InputHash{'METHOD'} eq 'CARD'))
		{
		my ($nStatus, $sError) = ProcessCardPayment($sOrderId);
		if ($nStatus != $::SUCCESS)
			{
			return($::FAILURE, $sError);
			}
		#
		# Fall through to receipt
		#
		LogData("PayPal::GetPspPage2: Finished Successfully");
		return ($::SUCCESS, '');
		}
	#
	# Get our PayPal order
	#
	my ($nStatus, $sError, $hOrderDetails) = GetPayPalOrder($sOrderId);
	if ($nStatus != $::SUCCESS)
		{
		return($::FAILURE, $sError);
		}
	#
	# Collect the address details - shipping and billing
	#
	($nStatus, $sError) = CollectContactDetails($hOrderDetails);
	#
	# If this is a checkout replacement call rather than a payment page
	# then display the shipping confirmation page whether error or not
	#
	if ($::g_InputHash{'PAGE'} ne 'PAYMENT')
		{
		#
		# We need to know if we can ship or not irrespective of the region which we may not know
		#
		if ($$::g_pSetupBlob{MAKE_SHIPPING_CHARGE})	# shipping is enabled
			{
			#
			# Do advanced shipping validation
			# Test first for conditions that are fatal then for warnings
			#
			my @Response = ActinicOrder::GetShippingPluginResponse();
			if ($Response[0] != $::SUCCESS)				# the script failed
				{
				return (ACTINIC::BounceToPageEnhanced(-1, $Response[1],
											$$::g_pSetupBlob{CHECKOUT_DESCRIPTION},
											$::g_sWebSiteUrl,
											$::g_sContentUrl, $::g_pSetupBlob, $::Session->GetLastShopPage(), \%::g_InputHash,
											$::FALSE));
				}
			elsif (${$Response[2]}{ValidateFinalInput} != $::SUCCESS)
				{
				return (ACTINIC::BounceToPageEnhanced(-1, ${$Response[3]}{ValidateFinalInput},
											$$::g_pSetupBlob{CHECKOUT_DESCRIPTION},
											$::g_sWebSiteUrl,
											$::g_sContentUrl, $::g_pSetupBlob, $::Session->GetLastShopPage(), \%::g_InputHash,
											$::FALSE));
				}
			elsif (${$Response[2]}{ValidatePreliminaryInput} != $::SUCCESS)
				{
				$sError .= ACTINIC::GetPhrase(-1, 1974) . ACTINIC::GetPhrase(-1, 1971, $::g_sRequiredColor) .
								ACTINIC::GetPhrase(-1, 102) . ACTINIC::GetPhrase(-1, 1975) . ACTINIC::GetPhrase(-1, 1970) . " - ". ${$Response[3]}{ValidatePreliminaryInput} . "<BR>\n";
				}
			}
		#
		# We need to record the authorized amount in case we need to
		# report the message $PayPal::REAUTHORISE_MESSAGE_FORMAT
		#
		$::Session->SetProviderParam($::PAYMENT_PAYPALCHECKOUT, $::PAYPAL_AUTHORISED, CurrencyToBaseUnits($hOrderDetails->{'purchase_units'}[0]{'amount'}{'value'}));
		my $sLogError = (length $sError) ? sprintf("(%s)", $sError) : '';
		LogData("PayPal::GetPspPage2: Finished $sLogError");
		return(main::DisplayShippingConfirmation($PayPal::TESTMESSAGE . $sError));
		}
	#
	# Return error if CollectContactDetails failed
	#
	if ($nStatus != $::SUCCESS)
		{
		return($::FAILURE, $sError);
		}
	($nStatus, $sError) = ValidateShippingAndTax();
	if ($nStatus != $::SUCCESS)
		{
		return($::FAILURE, $sError);
		}
	#
	# No errors so we can complete the sales order,
	#
	($nStatus, $sError) = main::CompleteOrder();
	if ($nStatus != $::SUCCESS)
		{
		#
		# Cannot complete the Sales Order
		#
		RecordErrors(sprintf("PayPal::GetPspPage2: Complete Order returned [%s]", $sError));
		return ($::FAILURE, $sError);
		}
	#
	# Record the payment
	#
	($nStatus, $sError) = CompletePayment($hOrderDetails);
	if ($nStatus != $::SUCCESS)
		{
		#
		# Cannot complete the Payment
		#
		RecordErrors(sprintf("PayPal::GetPspPage2: Complete Payment returned [%s]", $sError));
		return ($::FAILURE, $sError);
		}
	#
	# We need to put the order number in the input
	# hash to keep DisplayReceiptPhase happy
	# CompleteOrder will have called GetOrderNumber
	# which sets the order number in $::s_sOrderNumber
	#
	$::g_InputHash{'ORDERNUMBER'} = $::s_sOrderNumber;
	#
	# Update the checkout record and save the session
	# Do not rely on OrderScript to do it
	#
	main::UpdateCheckoutRecord();
	$::Session->SaveSession();

	LogData("PayPal::GetPspPage2: Finished Successfully");

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

###############################################################
#
# GetPspPage3 - Get the third PayPal page or validate the
#						order confirmation page if not already done
#
# Returns:	0 - status
#				1 - error message
#				2 - HTML of page IF error
#
###############################################################

sub GetPspPage3
	{
	LogData("PayPal::GetPspPage3: Started");
	#
	# Validate the shipping confirmation page
	# Re-display the page if error found
	#
	my ($nStatus, $sError);
	$sError = main::ValidateOrderConfirmation();
	if ($sError ne "")
		{
		return(main::DisplayShippingConfirmation($PayPal::TESTMESSAGE . $sError));
		}
 	#
	# Get the last saved Order ID
	#
	my $sOrderId = $::Session->GetProviderParam($::PAYMENT_PAYPALCHECKOUT, $::PAYPAL_ORDER_ID);
	#
	# Re-get the PayPal order details
	#
	my $hOrderDetails;
	($nStatus, $sError, $hOrderDetails) = GetPayPalOrder($sOrderId);
	if ($nStatus != $::SUCCESS)
		{
		return(main::DisplayShippingConfirmation($PayPal::TESTMESSAGE . $sError));
		}
	#
	# Create the Sellerdek Order (allocates the order number)
	#
	($nStatus, $sError) = main::CompleteOrder();
	if ($nStatus != $::SUCCESS)
		{
		#
		# Cannot complete the Sales Order
		#
		RecordErrors(sprintf("PayPal::GetPspPage3: Complete Order returned [%s]", $sError));
		return ($::FAILURE, $sError);
		}
	#
	# Record the payment
	#
	($nStatus, $sError) = CompletePayment($hOrderDetails);
	if ($nStatus != $::SUCCESS)
		{
		if ('PAYER_ACTION_REQUIRED' eq $sError)
			{
			my ($sAuthorisedAmount, $sRequiredAmount);
			my $nAuthorisedAmount = $::Session->GetProviderParam($::PAYMENT_PAYPALCHECKOUT, $::PAYPAL_AUTHORISED);
			($nStatus, $sError, $sAuthorisedAmount) = ActinicOrder::FormatPrice($nAuthorisedAmount, $::TRUE, $::g_pCatalogBlob);

			my $nRequiredAmount = CurrencyToBaseUnits($hOrderDetails->{'purchase_units'}[0]{'amount'}{'value'});
			($nStatus, $sError, $sRequiredAmount) = ActinicOrder::FormatPrice($nRequiredAmount, $::TRUE, $::g_pCatalogBlob);
			$sError = sprintf($PayPal::REAUTHORISE_MESSAGE_FORMAT, ACTINIC::HTMLEncode($sRequiredAmount), ACTINIC::HTMLEncode($sAuthorisedAmount));
			return(main::DisplayPage($PayPal::TESTMESSAGE . $sError, 2, $::FORWARD));
			}
		#
		# Cannot complete the Sales Order
		#
		RecordErrors(sprintf("PayPal::GetPspPage3: Complete Payment returned [%s]", $sError));
		return(main::DisplayShippingConfirmation($PayPal::TESTMESSAGE . $sError));
		}
	#
	# We need to put the order number in the input
	# hash to keep DisplayReceiptPhase happy
	# CompleteOrder will have called GetOrderNumber
	# which sets the order number in $::s_sOrderNumber
	#
	$::g_InputHash{'ORDERNUMBER'} = $::s_sOrderNumber;
	#
	# Update the checkout record and save the session
	# Do not rely on OrderScript to do it
	#
	main::UpdateCheckoutRecord();
	$::Session->SaveSession();

	LogData("PayPal::GetPspPage3: Completed successfully");
	return ($::SUCCESS, '');
	}

###############################################################
#
# ProcessWebhook - Process an incoming webhook
#
#						 IMPORTANT: This method must always end
#										by calling ExitWebhook()
#
# Returns:	0 - status
#				1 - error message
#				2 - HTML of page IF error
#
###############################################################

sub ProcessWebhook
	{
	LogData("PayPal::ProcessWebhook: ******************** STARTED *******************");
	$PayPal::IPN = $::TRUE;								# may be used to ensure validity of session
	if (1 > length $PayPal::PSP_WEBHOOKID)
		{
		ExitWebhook("Webhook ID is not known so ignoring the web hook request");
		}
	#
	# Get the POST data received for this request
	#
	my $sOrderNumber = '';
	my ($nStatus, $sError, $pWebhook) = ReadPostData();
	if ($nStatus == $::SUCCESS)
		{
		$PayPal::WEBHOOKMESSAGEID = $pWebhook->{'id'};
		#
		# Processing here depends on the type of event rather than
		# on the type of object but we shall note the object type for logging
		#
		LogData(sprintf("PayPal::ProcessWebhook: Received %s event",
					$pWebhook->{'event_type'}));
		#
		# Check for one of the supported event types
		#
		if (($pWebhook->{'event_type'} eq 'PAYMENT.AUTHORIZATION.CREATED') ||
			 ($pWebhook->{'event_type'} eq 'PAYMENT.AUTHORIZATION.VOIDED') ||
			 ($pWebhook->{'event_type'} eq 'PAYMENT.CAPTURE.COMPLETED') ||
			 ($pWebhook->{'event_type'} eq 'PAYMENT.CAPTURE.DENIED') ||
			 ($pWebhook->{'event_type'} eq 'PAYMENT.CAPTURE.PENDING') ||
			 ($pWebhook->{'event_type'} eq 'PAYMENT.CAPTURE.REFUNDED') ||
			 ($pWebhook->{'event_type'} eq 'PAYMENT.CAPTURE.REVERSED'))
			{
			my $bSendMail = $::FALSE;
			#
			# Webhooks may arrive out of sequence so we shall re-get
			# the appropriate object using the 'self' link if available
			#
			my $pResource;
			($nStatus, $sError, $pResource) = GetResource('self', $pWebhook->{'resource'}{'links'});
			if ($nStatus != $::SUCCESS)
				{
				ExitWebhook($sError);
				}
			$sOrderNumber = $pResource->{'invoice_id'};
			my ($sOrderId, $sCardDetails, $sIntent);
			#
			# To be able to record the authorisation we need the authorise file
			# which we can get using the order number (invoice) of the 'self' response
			# Silently ignore Webhook if authorise file not found as probably the
			# payment was recorded in the session file thus allowing the file to be removed
			#
			($nStatus, $sError) = LoadAuthoriseFile($sOrderNumber);
			if ($nStatus != $::SUCCESS)
				{
				LogData('PayPal::ProcessWebhook: No authorisation file, ok if refund/charge or void request');
				}
			else
				{
				#
				# Get the PayPal order
				#
				my ($pOrderDetails);
				$sOrderId = $::Session->GetProviderParam($::PAYMENT_PAYPALCHECKOUT, $::PAYPAL_ORDER_ID);
				($nStatus, $sError, $pOrderDetails) = GetPayPalOrder($sOrderId);
				if ($nStatus != $::SUCCESS)
					{
					LogData(sprintf("PayPal::ProcessWebhook: GetPayPalOrder(%s) returned error %s", $sOrderId, $sError));
					}
				else
					{
					$sCardDetails = GetCardDetails($pOrderDetails->{'payment_source'});
					$sIntent = $pOrderDetails->{'intent'};
					}
				#
				# Time to send the email receipt if this is an authorization confirmation
				# or, if capturing immediately, and this is a capture confirmation
				#
				if (($pWebhook->{'event_type'} eq 'PAYMENT.AUTHORIZATION.CREATED') ||
					 (($pWebhook->{'event_type'} eq 'PAYMENT.CAPTURE.COMPLETED') &&
					  ($sIntent eq 'CAPTURE')))
					{
					$bSendMail = IsEmailRequired($pResource->{'amount'});
					}
				}

			if ($pWebhook->{'resource_type'} eq 'authorization')
				{
				#
				# Create an OCC file for the authorisation
				#
				($nStatus, $sError) = RecordAuthorization(GetParamsFromAuthorization($sOrderId, $pResource), $sCardDetails);
				if ($nStatus != $::SUCCESS)
					{
					ExitWebhook($sError);
					}
				}
			elsif ($pWebhook->{'resource_type'} eq 'capture')
				{
				my $sAuthId = '';
				if ($sIntent eq 'AUTHORIZE')
					{
					#
					# Get the corresponding authorisation (parent) ID
					# This is only applicable for AUTHORIZE intent orders
					#
					my ($pAuthDetails);
					($nStatus, $sError, $pAuthDetails) = GetResource('up', $pWebhook->{'resource'}{'links'});
					if ($nStatus != $::SUCCESS)
						{
						ExitWebhook($sError);
						}
					$sAuthId = $pAuthDetails->{'id'};
					}
				#
				# Create an OCC file for the capture
				#
				($nStatus, $sError) = RecordAuthorization(GetParamsFromCapture($sOrderId, $pResource, $sAuthId), $sCardDetails);
				if ($nStatus != $::SUCCESS)
					{
					ExitWebhook($sError);
					}
				}
			elsif ($pWebhook->{'resource_type'} eq 'refund')
				{
				#
				# Create an OCC file for the refund
				#
				($nStatus, $sError) = RecordAuthorization(GetParamsFromRefund($sOrderId, $pResource));
				if ($nStatus != $::SUCCESS)
					{
					ExitWebhook($sError);
					}
				}
			elsif ($pWebhook->{'resource_type'} eq 'void')
				{
				#
				# Create an OCC file for the voiding
				#
				($nStatus, $sError) = RecordAuthorization(GetParamsFromVoid($sOrderId, $pResource));
				if ($nStatus != $::SUCCESS)
					{
					ExitWebhook($sError);
					}
				}
			else
				{
				LogData('PayPal::ProcessWebhook: Ignoring resource type ' . $pWebhook->{'resource_type'});
				}
			if ($bSendMail)
				{
				main::EmailReceipt($sOrderNumber, $::TRUE, '');
				LogData(sprintf("PayPal::ProcessWebhook: Sent receipt email"));
				}
			}
		else
			{
			LogData('PayPal::ProcessWebhook: Ignoring event type ' . $pWebhook->{'event_type'});
			}
		}

	ExitWebhook('');
	}

###############################################################
#
# ExitWebhook - Handle the web hook completion
#
# Params:	$sError	- Error message if any
#
# Returns:	Does not return
#
###############################################################

sub ExitWebhook
	{
	my $sError = shift;
	if (length $sError)
		{
		RecordErrors("PayPal::ExitWebhook: $sError");
		}
	else
		{
		if (defined $::Session)
			{
			main::UpdateCheckoutRecord();				# update the checkout
			$::Session->SaveSession();					# save the session before exiting
			}
		}
	#
	# We only need to respond with a '200 OK'
	#
	ACTINIC::PrintText('');
	LogData("PayPal::ProcessWebhook: ******************** FINISHED *******************");

	exit;
	}

###############################################################
#
# GetResource - Get a resource using an array of links
#
# Params:	$sLinkKey	- link key to fetch
#				$pLinks		- hash of link objects
#
# Returns:	0 - status
#				1 - error message
#				2 - pointer to hash of response
#
###############################################################

sub GetResource
	{
	my $sLinkKey = shift;
	my $pLinks = shift;

	my ($sUrl, $sMode) = GetUrlFromLinks($sLinkKey, $pLinks);
	if (!defined $sUrl)
		{
		my $sError = "Could not get the resource 'self' URL";
		RecordErrors("PayPal::GetResource: " . $sError);
		return($::FAILURE, $sError);
		}
	#
	# If we have a URL then attempt to call it
	#
	use URI::Split qw(uri_split);
	my ($sScheme, $sAuth, $sPath, $sQuery, $sFrag) = uri_split($sUrl);
	my ($nStatus, $sError, $pResource) = SendRequest($sAuth, $PayPal::PORT, $sPath, $sMode);
	if ($nStatus != $::SUCCESS)
		{
		$sError = "Could not get the resource '$sLinkKey' URL $sUrl";
		RecordErrors("PayPal::GetResource: " . $sError);
		return($::FAILURE, $sError);
		}
	return($::SUCCESS, '', $pResource);
	}

#######################################################
#
# ProcessCardPayment - PayPal page 2 captures the card
#					 			payment for the payment page
#
#	Input: 	$sPayPalOrderId - PayPal order ID
#
#	Returns:	0 - status $::SUCCESS or $::FAILURE
#				1 - error message if status not $::SUCCESS
#				2 - HTML of page IF error
#
#######################################################

sub ProcessCardPayment
	{
	LogData("PayPal::ProcessCardPayment: Started");
	my $sPayPalOrderId = shift;
	#
	# 3DS results
	# https://developer.paypal.com/docs/checkout/advanced/customize/3d-secure/response-parameters/
	#
	my $bPaymentAccepted = $::FALSE;
	my $sOrderId = $::Session->GetProviderParam($::PAYMENT_PAYPALCHECKOUT, $::PAYPAL_ORDER_ID);
	my ($nStatus, $sError, $hPayPalOrderDetails) = SendApiRequest('checkout/orders/' . $sPayPalOrderId . '?fields=payment_source', 'GET');
	if ($nStatus != $::SUCCESS)
		{
		return($::FAILURE, $PayPal::PAYPAL_ERROR);
		}

	my $s3DsLiabilityShifted = $hPayPalOrderDetails->{'payment_source'}{'card'}{'authentication_result'}{'liability_shift'};
	my $s3DsEnrolled = $hPayPalOrderDetails->{'payment_source'}{'card'}{'authentication_result'}{'three_d_secure'}{'enrollment_status'};
	my $s3DsStatus = $hPayPalOrderDetails->{'payment_source'}{'card'}{'authentication_result'}{'three_d_secure'}{'authentication_status'};
	LogData(sprintf("PayPal::ProcessCardPayment:3DS [%s] [%s] [%s]", $s3DsEnrolled, $s3DsStatus, $s3DsLiabilityShifted));
	my $s3DS = $s3DsEnrolled . $s3DsStatus . $s3DsLiabilityShifted;
	#
	# Check and interpret the 3DS fields
	#
	if (('YYPOSSIBLE' eq $s3DS) ||
		 ('YAPOSSIBLE' eq $s3DS))
		{
		$bPaymentAccepted = $::TRUE;
		}
	else
		{
		$s3DS = $s3DsEnrolled . $s3DsLiabilityShifted;
		if (('NNO' eq $s3DS) ||
			 ('UNO' eq $s3DS) ||
			 ('BNO' eq $s3DS))
			{
			if ($PayPal::PSP_3DS_ACCEPT_ENROLLED)
				{
				$bPaymentAccepted = $::TRUE;
				}
			else
				{
				LogData('PayPal::ProcessCardPayment:Liability declined by merchant');
				return ($::FAILURE, $PayPal::PAYPAL_ERROR);
				}
			}
		}
	#
	# Reject everything else
	#
	if (!$bPaymentAccepted)
		{
		LogData('PayPal::ProcessCardPayment:Liability declined');
		return ($::FAILURE, $PayPal::PAYPAL_ERROR);
		}
	my $s3DsResult = 'L.Shift:' . $s3DsLiabilityShifted;
	#
	# Ideally we should fetch the PayPal Order to determine the Intent of the order but
	# For Custom Card Fields the order is still at status CREATED
	#
	if ($PayPal::INTENT eq 'AUTHORIZE')
		{
		#
		# Authorise the order
		# Returns the updated PayPal order details
		#
		my ($nStatus, $sError, $hPayPalOrderDetails) = SendApiRequest('checkout/orders/' . $sPayPalOrderId . '/authorize', 'POST');
		if ($nStatus != $::SUCCESS)
			{
			return ($nStatus, $sError);
			}
		#
		# Get the authorisation ID
		# By design there should only be one authorisation on the PayPal order
		#
		my $sAuthorisationId = $hPayPalOrderDetails->{'purchase_units'}[0]{'payments'}{'authorizations'}[0]{'id'};
		#
		# Record the authorization
		#
		($nStatus, $sError) = RecordPayPalAuthorization($sPayPalOrderId, $sAuthorisationId, $s3DsEnrolled, $s3DsResult);
		if ($nStatus != $::SUCCESS)
			{
			return ($nStatus, $sError);
			}
		}
	else
		{
		#
		# Capture the payment
		# Returns the updated PayPal order details
		#
		my ($nStatus, $sError, $hPayPalOrderDetails) = SendApiRequest('checkout/orders/' . $sPayPalOrderId . '/capture', 'POST');
		if ($nStatus != $::SUCCESS)
			{
			return ($nStatus, $sError);
			}
		#
		# Get the capture ID
		# By design there should only be one capture on the PayPal order
		#
		my $sCaptureId = $hPayPalOrderDetails->{'purchase_units'}[0]{'payments'}{'captures'}[0]{'id'};
		#
		# Record the capture
		#
		($nStatus, $sError) = RecordPayPalCapture($sPayPalOrderId, $sCaptureId, $s3DsEnrolled, $s3DsResult);
		if ($nStatus != $::SUCCESS)
			{
			return ($nStatus, $sError);
			}
		}
	#
	# Clone the session file to a .authorise file
	# and saves DD email if required
	# Do not set the PSP Requested flag, it only applies when
	# bouncing to a hosted payment page
	#
	main::CloneSession($::FALSE);
	#
	# We need to put the order number in the input
	# hash to keep DisplayReceiptPhase happy
	#
	$::g_InputHash{'ORDERNUMBER'} = $::g_PaymentInfo{'ORDERNUMBER'};

	LogData("PayPal::ProcessCardPayment: Finished Successfully");

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

#######################################################
#
# IsEmailRequired - Check if email is required
#
#	Input:	0 - hash of currency_code and value
#
#	Returns:	0 - true if email can be sent
#
#######################################################

sub IsEmailRequired
	{
	my $pAmount = shift;
	#
	# We can send an email if not yet sent and full payment captured, we are
	# processing a webhook and we still have the authorise file
	#
	return (defined $::Session &&
 		$::Session->IsAuthorisationFile() &&
 		$Session::MAIL_STATE_UNDEF == $::Session->GetMailState() &&
 		IsIpnRequest() &&
		IsSameCurrency($pAmount->{'currency_code'}) &&
 		IsFullyPaid(CurrencyToBaseUnits($pAmount->{'value'})));
	}

#######################################################
#
# IsSameCurrency - Check if payment currency is as expected
#
#	Input:	0 - currency code
#
#	Returns:	0 - true if same as expected
#
#######################################################

sub IsSameCurrency
	{
	my $sCurrency = shift;
	if ($sCurrency ne GetOrderCurrencyCode())
		{
		#
		# Not fully paid if different currency
		#
		return ($::FALSE);
		}
	return($::TRUE);
	}

#######################################################
#
# IsFullyPaid - Determine if full payment received
#
# Params:	0 - Captured amount in base units (pence/cents)
#
# Returns:	0 - $::TRUE if fully paid
#
#######################################################

sub IsFullyPaid
	{
	my $nAmount = shift;

	my $bFullyPaid = $::FALSE;							# assume not fully paid
	#
	# If we do not have a session file then
	# we cannot check for fully paid
	#
	if (defined $::Session)
		{
		#
		# If this is an IPN request and we don't have an authorize 
		# session file then we cannot check for fully paid
		#
		if (!IsIpnRequest() ||
			($::Session->IsAuthorisationFile()))
			{
			#
			# Order is fully paid if order total
			# is not less than the captured amount
			#
			if ($nAmount >= $::g_PaymentInfo{'ORDERTOTAL'})
				{
				$bFullyPaid = $::TRUE;
				}
			else
				{
				LogData(sprintf("PayPal::IsFullyPaid: Expected %s but received %s", $::g_PaymentInfo{'ORDERTOTAL'}, $nAmount));
				}
			}
		}
	return($bFullyPaid);
	}

#######################################################
#
# IsIpnRequest - Check if this is an IPN being processed
#
# Returns:	0 - $::TRUE if IPN request
#
#######################################################

sub IsIpnRequest
	{
	return ($PayPal::IPN);
	}

#######################################################
#
# GetUrlFromLinks - Get a URL from the Links array
#
#	Input:	0 - Name of link to fetch (rel param)
#				1 - Pointer to the array of links
#
#	Returns:	0 - URL or undef if not found
#
#######################################################

sub GetUrlFromLinks
	{
	#
	# Receives a list (array) of link objects
	#
	my ($sRel) = shift;
	my ($paLinks) = shift;
	#
	# Examine each link object in the array
	#
	foreach (@{$paLinks})
		{
		my $pObject = $_;
		#
		# If object is a hash and the 'rel' element
		# is the one we are looking for then
		# return the href
		#
		if ((ref($pObject) eq 'HASH') &&
			 ($pObject->{'rel'} eq $sRel))
			{
			return ($pObject->{'href'});
			}
		}
	return (undef);
	}

#######################################################
#
# CompletePayment - Complete the PayPal order by
#							requesting the authorise or capture
#
#	Input:	0 - PayPal order details hash
#				1 - Attempt authorise/capture, default true
#
#	Returns:	0 - status $::SUCCESS or $::FAILURE
#				1 - error message if status not $::SUCCESS
#
#######################################################

sub CompletePayment
	{
	my $hPayPalOrderDetails = shift;
	my $bAuthorise = shift;
	if (!defined $bAuthorise)
		{
		$bAuthorise = $::TRUE;
		}
	LogData("PayPal::CompletePayment: Started");
	my ($nStatus, $sError);

	my $sPayPalOrderId = $hPayPalOrderDetails->{'id'};
	#
	# Sets the final order total and order number
	#
	($nStatus, $sError, $hPayPalOrderDetails) = SetFinalOrderDetails($hPayPalOrderDetails);
	if ($nStatus != $::SUCCESS)
		{
		return ($nStatus, $sError);
		}
	#
	# Clone the session file to a .authorise file
	# and saves DD email if required
	# Do not set the PSP Requested flag, it only applies when
	# bouncing to a hosted payment page
	#
	main::CloneSession($::FALSE);
	#
	# If the PayPal Order status is APPROVED then we can progress the order
	# If the PayPal Order status is COMPLETED then we shall simply create a new confirmation (OCC)
	#
	if (($hPayPalOrderDetails->{'status'} eq 'APPROVED') ||
		 ($hPayPalOrderDetails->{'status'} eq 'COMPLETED'))
		{
		if ($hPayPalOrderDetails->{'intent'} eq 'AUTHORIZE')
			{
			if ($hPayPalOrderDetails->{'status'} eq 'APPROVED')
				{
				#
				# Authorise the order (calls PayPal)
				# Returns the updated PayPal order details
				#
				($nStatus, $sError, $hPayPalOrderDetails) = SendApiRequest('checkout/orders/' . $sPayPalOrderId . '/authorize', 'POST');
				if ($nStatus != $::SUCCESS)
					{
					return ($nStatus, $sError);
					}
				}
			else
				{
				LogData('PayPal::CompletePayment: Skipping authorisation as status is ' . $hPayPalOrderDetails->{'status'});
				}
			#
			# Get the authorisation ID
			# By design there should only be one authorisation on the PayPal order
			#
			my $sAuthorisationId = $hPayPalOrderDetails->{'purchase_units'}[0]{'payments'}{'authorizations'}[0]{'id'};
			#
			# Record the authorization
			#
			($nStatus, $sError) = RecordPayPalAuthorization($sPayPalOrderId, $sAuthorisationId);
			if ($nStatus != $::SUCCESS)
				{
				return ($nStatus, $sError);
				}
			}
		elsif ($hPayPalOrderDetails->{'intent'} eq 'CAPTURE')
			{
			if ($hPayPalOrderDetails->{'status'} eq 'APPROVED')
				{
				#
				# Capture the payment (calls PayPal)
				# Returns the updated PayPal order details
				#
				($nStatus, $sError, $hPayPalOrderDetails) = SendApiRequest('checkout/orders/' . $sPayPalOrderId . '/capture', 'POST');
				if ($nStatus != $::SUCCESS)
					{
					return ($nStatus, $sError);
					}
				}
			else
				{
				LogData('PayPal::CompletePayment: Skipping capture as status is ' . $hPayPalOrderDetails->{'status'});
				}
			#
			# Get the capture ID
			# By design there should only be one capture on the PayPal order
			#
			my $sCaptureId = $hPayPalOrderDetails->{'purchase_units'}[0]{'payments'}{'captures'}[0]{'id'};
			#
			# Record the capture
			#
			($nStatus, $sError) = RecordPayPalCapture($sPayPalOrderId, $sCaptureId);
			if ($nStatus != $::SUCCESS)
				{
				return ($nStatus, $sError);
				}
			}
		else
			{
			RecordErrors(sprintf("PayPal::CompletePayment: Unexpected intent '%s' on PayPal order %s", $hPayPalOrderDetails->{'intent'}, $sPayPalOrderId));
			return ($::FAILURE, $PayPal::PAYPAL_ERROR);
			}
		}
	elsif ($bAuthorise)
		{
		#
		# If the status is not APPROVED or COMPLETED then it is okay
		# but only if we are not expecting to authorise at this time
		#
		RecordErrors(sprintf("PayPal::CompletePayment: Unexpected status '%s' on PayPal order %s", $hPayPalOrderDetails->{'status'}, $sPayPalOrderId));
		return ($::FAILURE, $PayPal::PAYPAL_ERROR);
		}
	LogData('PayPal::CompletePayment: Completed successfully');

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

#######################################################
#
# GetPayPalOrder - Get a PayPal order
#
#	Input:	0 - order ID
#
#	Returns:	0 - status $::SUCCESS or $::FAILURE
#				1 - error message if status not $::SUCCESS
#				2 - PayPal order details hash if $::SUCCESS
#
#######################################################

sub GetPayPalOrder
	{
	my $sOrderId = shift;
	if (!defined $sOrderId ||
		 length $sOrderId < 1)
		{
		#
		# No order Id passed, return error
		#
		return ($::FAILURE, 'Cannot lookup null Order Id');
		}
	return (SendApiRequest('checkout/orders/' . $sOrderId, 'GET'));
	}

#######################################################
#
# CreatePayPalOrder - Create a PayPal order
#
#	Params:	0 - true if coming from payment page
#
#	Returns:	0 - status $::SUCCESS or $::FAILURE
#				1 - error message if status not $::SUCCESS
#				2 - PayPal order details hash if $::SUCCESS
#
#######################################################

sub CreatePayPalOrder
	{
	my $bPaymentPage = shift;
	#
	# Create a new PayPal Order
	#
	my ($nStatus, $sError, $hOrderDetails, $paItems);
	#
	# Get the cart details
	# We shall get the total discount rather than showing as a
	# separate line as PayPal does not llow negative line items
	#
	my ($nTotalDiscount, $nItemTotal);
	($nStatus, $sError, $paItems, $nItemTotal, $nTotalDiscount) = AddOrderLineItems(GetOrderCurrencyCode($::FALSE));

	my (%hParams) =	(
							'intent' => $PayPal::INTENT,
							'purchase_units' => 
								[{'amount' =>
									{
									'currency_code' => GetOrderCurrencyCode(),
									'value' => BaseUnitsToCurrency($::g_PaymentInfo{'ORDERTOTAL'}),
									'breakdown' => 
										{'item_total' =>
											{
											'currency_code' => GetOrderCurrencyCode(),
											'value' => BaseUnitsToCurrency($nItemTotal)
											}
										}
									}
								}]
							);
	#
	# Override the account brand name if requested
	#
	if (length $PayPal::PSP_BRAND_NAME)
		{
		$hParams{'application_context'}{'brand_name'} = $PayPal::PSP_BRAND_NAME;
		}
	#
	# Override the account landing page if requested
	# Possible values are LOGIN, BILLING or NO_PREFERENCE (default)
	#
	if ($PayPal::PSP_LANDING_PAGE == 1)
		{
		$hParams{'application_context'}{'landing_page'} = 'LOGIN';
		}
	elsif ($PayPal::PSP_LANDING_PAGE == 2)
		{
		$hParams{'application_context'}{'landing_page'} = 'BILLING';
		}
	#
	# Set Shipping address preferences
	# Possible values are GET_FROM_FILE (default), NO_SHIPPING, SET_PROVIDED_ADDRESS
	#
	if ($bPaymentPage ||
		$ACTINIC::B2B->Get('UserDigest'))
		{
		#
		# For payment page and logged in users we want to specify the addresses
		# First the payer (bill-to)
		#
		# Get the ISO invoice country code
		#
		my $sBillCountryCode = ActinicLocations::GetISOInvoiceCountryCode();
		#
		# Get the invoice state/province
		# use the region code (state code) for United States
		#
		my $sBillStateProvince = ($sBillCountryCode eq 'US') ? ActinicLocations::GetISOInvoiceRegionCode() : $::g_BillContact{'ADDRESS4'};

		my $sBillingPhoneNumber = ParsePhoneNumber($::g_BillContact{'PHONE'});
		my $sBillingMobileNumber = ParsePhoneNumber($::g_BillContact{'MOBILE'});
		#
		# PayPal ignores th county unless uppercase and matches their list
		# PayPal rejects the post code if not uppercase
		#
		if ($$::g_pSetupBlob{'SHOPPER_NAME_HANDLING_MODE'} eq 1)	# first name/ last name handling
			{
			$hParams{'payer'}{'name'}{'given_name'}	= $::g_BillContact{'FIRSTNAME'};
			$hParams{'payer'}{'name'}{'surname'}		= $::g_BillContact{'LASTNAME'};
			}
		else
			{
			$hParams{'payer'}{'name'}{'given_name'}, $hParams{'payer'}{'name'}{'surname'} = ActinicOrder::SplitCustomerName($::g_BillContact{'NAME'});
			}
		$hParams{'payer'}{'email_address'}					= $::g_BillContact{'EMAIL'};

		if (length $sBillingMobileNumber)
			{
			$hParams{'payer'}{'phone'}{'phone_type'}		= 'MOBILE';
			$hParams{'payer'}{'phone'}{'phone_number'}{'national_number'} = $sBillingMobileNumber;
			}
		elsif (length $sBillingPhoneNumber)
			{
			$hParams{'payer'}{'phone'}{'phone_type'}		= 'HOME';
			$hParams{'payer'}{'phone'}{'phone_number'}{'national_number'} = $sBillingPhoneNumber;
			}

		$hParams{'payer'}{'address'}{'address_line_1'}	= $::g_BillContact{'ADDRESS1'};
		$hParams{'payer'}{'address'}{'address_line_2'}	= $::g_BillContact{'ADDRESS2'};
		$hParams{'payer'}{'address'}{'admin_area_2'}		= $::g_BillContact{'ADDRESS3'};
		$hParams{'payer'}{'address'}{'admin_area_1'}		= uc($sBillStateProvince);
		$hParams{'payer'}{'address'}{'postal_code'}		= uc($::g_BillContact{'POSTALCODE'});
		$hParams{'payer'}{'address'}{'country_code'}		= $sBillCountryCode;
		#
		# Do not pass the shipping address for registered customers
		# This is to mimick the old behaviour
		# At some point we should honour the B2B setting of not allowing
		# a different delivery address
		#
		if ($ACTINIC::B2B->Get('UserDigest') eq '')
			{
			$hParams{'application_context'}{'shipping_preference'}	= 'SET_PROVIDED_ADDRESS';
			#
			# Get the ISO shipping country code
			#
			my $sShipCountryCode = ActinicLocations::GetISODeliveryCountryCode();
			#
			# Get the shipping state/province
			# use the region code (state code) for United States
			#
			my $sShipStateProvince = ($sShipCountryCode eq 'US') ? ActinicLocations::GetISODeliveryRegionCode() : $::g_ShipContact{'ADDRESS4'};
			#
			# PayPal ignores the county unless uppercase and matches their list
			# PayPal rejects the post code if not uppercase
			#
			$hParams{'purchase_units'}[0]{'shipping'}{'name'}{'full_name'}				= $::g_ShipContact{'NAME'};
			$hParams{'purchase_units'}[0]{'shipping'}{'address'}{'address_line_1'}	= $::g_ShipContact{'ADDRESS1'};
			$hParams{'purchase_units'}[0]{'shipping'}{'address'}{'address_line_2'}	= $::g_ShipContact{'ADDRESS2'};
			$hParams{'purchase_units'}[0]{'shipping'}{'address'}{'admin_area_2'}		= $::g_ShipContact{'ADDRESS3'};
			$hParams{'purchase_units'}[0]{'shipping'}{'address'}{'admin_area_1'}		= uc($sShipStateProvince);
			$hParams{'purchase_units'}[0]{'shipping'}{'address'}{'postal_code'}		= uc($::g_ShipContact{'POSTALCODE'});
			$hParams{'purchase_units'}[0]{'shipping'}{'address'}{'country_code'}		= $sShipCountryCode;
			}
		else
			{
			$hParams{'application_context'}{'shipping_preference'} = 'GET_FROM_FILE';
			}
		}
	else
		{
		#
		# For cart and start checkout pages we want to use the PayPal users shipping address
		#
		$hParams{'application_context'}{'shipping_preference'} = 'GET_FROM_FILE';
		}
	#
	# If no tangible products in the cart then do not request a shipping address
	#
	if (!ACTINIC::CartHasTangibleGoods())
		{
		$hParams{'application_context'}{'shipping_preference'} = 'NO_SHIPPING';
		}
	#
	# For checkout we want to use CONTINUE as we are not ready yet to take the payment
	# Possible values are CONTINUE (default), PAY_NOW
	#
	if ($bPaymentPage)
		{
		$hParams{'application_context'}{'user_action'} = 'PAY_NOW';
		}
	#
	# Choose to allow asynchronous requests. Selection is based on INTENT
	# Possible values are UNRESTRICTED (default), IMMEDIATE_PAYMENT_REQUIRED
	#
	$hParams{'application_context'}{'payment_method'}{'payee_preferred'} = $PayPal::PAYEE_PREFERRED;
	$hParams{'purchase_units'}[0]{'items'} = $paItems;
	#
	# Add the discount if not zero
	#
	if ($nTotalDiscount != 0)
		{
		$hParams{'purchase_units'}[0]{'amount'}{'breakdown'}{'discount'}{'currency_code'} = GetOrderCurrencyCode();
		$hParams{'purchase_units'}[0]{'amount'}{'breakdown'}{'discount'}{'value'} = BaseUnitsToCurrency($nTotalDiscount);
		}
	#
	# If we are on the Payment Page and we have an Order ID then set it as the Invoice ID
	#
	if ($bPaymentPage &&
		 (length $::g_PaymentInfo{'ORDERNUMBER'} > 0))
		{
		$hParams{'purchase_units'}[0]{'invoice_id'} = $::g_PaymentInfo{'ORDERNUMBER'};
		}
	#
	# Create the payPal Order
	#
	my ($nStatus, $sError, $hOrderDetails) = (SendApiRequest('checkout/orders', 'POST', \%hParams));

	return ($nStatus, $sError, $hOrderDetails);
	}

#######################################################
#
# ValidateShippingAndTax - Validate shipping and tax
#
#	Returns:	0 - status $::SUCCESS or $::FAILURE
#				1 - error message if status not $::SUCCESS
#
#######################################################

sub ValidateShippingAndTax
	{
	my $parrInputPhases = main::GetPhaseListFromInput();
	#
	# Validate each phase in the current block
	#
	my $bActuallyValidate = $::TRUE;
	my ($nPhase, $sError);
	foreach $nPhase (@$parrInputPhases)
		{
		#
		# Dispatch the page-specific data
		# Note that PayPal is only concerned with
		# Shpping and Tax
		#
		if ($nPhase == $::SHIPCHARGEPHASE)
			{
			$sError = main::ValidateShipCharge($bActuallyValidate);
			}
		elsif ($nPhase == $::TAXCHARGEPHASE)
			{
			#
			# Only try to validate the tax if the shipping was okay
			#
			if (length $sError < 1)
				{
				$sError = ActinicOrder::ValidateTax($bActuallyValidate);
				}
			}
		}
	my $nStatus = (length $sError < 1) ? $::SUCCESS : $::FAILURE;
	return ($nStatus, $sError);
	}

###############################################################
#
# ParsePhoneNumber - Parse the phone number
#
#	Input:	0 - a phone number
#
#	Returns:	0 - parsed number
#
###############################################################

sub ParsePhoneNumber
	{
	my $sPhoneNumber = shift;

	$sPhoneNumber =~ s/\D//g;							# only digits
	$sPhoneNumber =~ s/^0+//g;							# loose any leading zeroes
	$sPhoneNumber = substr($sPhoneNumber, 0, 14);# only first 14 digits
	return ($sPhoneNumber);
	}

###############################################################
#
# CollectContactDetails - Save the contact details to hashes
#
#	Shipping Address is set as follows: -
#
#	If the cart has no tangible products (shipment is not required)
#
#		Sellerdeck collected addresses (Checkout Page 2)
#			- use Sellerdeck billing address as shipping address
#
#		PayPal collected addresses (Checkout Page 0 and View Cart)
#			- use our billing address as shipping address only if logged in
#			- if not logged in we have no addresses - should not occur as
#				we do not allow PayPal on View Cart or Checkout Page 0 if
#				shipment is not required unless user is logged in
#
#	If the cart does have tangible products (shipment is required)
#
#		Sellerdeck collected addresses (Checkout Page 2)
#			- check shipping address was not changed in payPal order
#
#		PayPal collected addresses (Checkout Page 0 and View Cart)
#			- get shipping address from the PayPal order
#
#	Copy shipping address to billing address if not logged in and tangible cart
#
#	Input:	0 - PayPal order details hash
#
#	Returns:	0 - status
#				1 - error message
#
###############################################################

sub CollectContactDetails
	{
	my $hOrderDetails = shift;
	#
	# Check if the call is from the payment page
	#
	my $bPaymentPage = ($::g_InputHash{'PAGE'} eq 'PAYMENT') ? $::TRUE : $::FALSE;
	#
	# Check if we should use our own addresses
	# Use our billing address for Payment Page and registered customers
	#
	my $bUseOurBillingAddress = ($bPaymentPage || ($ACTINIC::B2B->Get('UserDigest') ne '')) ? $::TRUE : $::FALSE;
	#
	# Use our shipping address for Payment Page
	#
	my $bUseOurShippingAddress = $bPaymentPage;
	#
	# Check if the cart has tangible products
	#
	my $bCartHasTangibleGoods = ACTINIC::CartHasTangibleGoods();
	#
	# Get the Shipping address hash if we have tangible goods
	#
	if ($bCartHasTangibleGoods)
		{
		my $hShipAddress = $hOrderDetails->{'purchase_units'}[0]{'shipping'};
		if (!defined $hShipAddress)
			{
			LogData("PayPal::CollectContactDetails: Shipping Address not found on PayPal Order");
			return ($::FAILURE, $PayPal::PAYPAL_ERROR);
			}
		my %hPreviousShipContact;
		ACTINIC::CopyHash(\%::g_ShipContact, \%hPreviousShipContact, '',  '');
		#
		# Load the shipping address
		#
		GetShippingAddress($hShipAddress, $bUseOurShippingAddress);
		#
		# Now we do have a shipping address that we
		# can use as the billing address if necessary
		#
		$bUseOurShippingAddress = $::TRUE;
		}
	#
	# Get the Billing address hash, may not be present for cards
	# which is okay so long as we have our own billing address
	#
	my $hBillAddress = $hOrderDetails->{'payer'};
	if ((!defined $hBillAddress) &&
		 (!$bUseOurBillingAddress))
		{
		LogData("PayPal::CollectContactDetails: Billing Address not found on PayPal Order");
		return ($::FAILURE, $PayPal::PAYPAL_ERROR);
		}
	#
	# Load the billing address
	#
	GetBillingAddress($hBillAddress, $bUseOurBillingAddress, $bUseOurShippingAddress);

	if (!$bCartHasTangibleGoods)
		{
		#
		# Cart has no tangible products so PayPal will not have either address
		# Use our billing address, if we have one, as the shipping address
		#
		LogData('PayPal::CollectContactDetails: Using Billing address as Shipping address');
		ACTINIC::CopyHash(\%::g_BillContact, \%::g_ShipContact, '',  '');
		}
	#
	# If any shipping name or address fields are different from
	# the billing address then set the separate delivery address flag
	# but only if we are not on the payment page
	#
	if (!$bPaymentPage)
		{
		foreach (@ActinicOrder::arrAddressMatchingKeys)
			{
			if (uc($::g_BillContact{$_}) ne uc($::g_ShipContact{$_}))
				{
				LogData("PayPal::CollectContactDetails: Compared $_ of \$::g_BillContact with \$::g_ShipContact [" . $::g_BillContact{$_} . '] <> [' . $::g_ShipContact{$_} . ']');
				$::g_BillContact{'SEPARATE'} = $::g_LocationInfo{'SEPARATESHIP'} = $::TRUE;
				last;
				}
			}
		}
	#
	# If not the Payment Page and not using seperate addresses
	# copy the billing email and phone details to the shipping address
	#
	if (!$bPaymentPage &&
		 !$::g_LocationInfo{'SEPARATESHIP'})
		{
		LogData('PayPal::CollectContactDetails: Using billing phone/email details');
		$::g_ShipContact{'EMAIL'}	= $::g_BillContact{'EMAIL'};
		$::g_ShipContact{'MOBILE'}	= $::g_BillContact{'MOBILE'};
		$::g_ShipContact{'PHONE'}	= $::g_BillContact{'PHONE'};
		$::g_ShipContact{'FAX'}		= $::g_BillContact{'FAX'};
		}
	else
		{
		LogData('PayPal::CollectContactDetails: Not using billing phone/email details');
		}
	#
	# Sort out the location info for the addresses
	#
	$::g_LocationInfo{DELIVERY_REGION_CODE}	= ActinicOrder::GetSellerDeckRegion($::g_LocationInfo{'DELIVERY_COUNTRY_CODE'}, $::g_ShipContact{'ADDRESS4'}, $::g_ShipContact{'POSTALCODE'});
	$::g_LocationInfo{INVOICE_REGION_CODE}		= ActinicOrder::GetSellerDeckRegion($::g_LocationInfo{'INVOICE_COUNTRY_CODE'}, $::g_BillContact{'ADDRESS4'}, $::g_BillContact{'POSTALCODE'});

	if ($::g_LocationInfo{DELIVERY_REGION_CODE} eq $ActinicOrder::UNDEFINED_REGION)
		{
		#
		# Region is undefined, check if need it
		#
		if (ActinicOrder::StateRequiredForValidation('Delivery', $::g_LocationInfo{DELIVERY_COUNTRY_CODE}))
			{
			#
			# Does not matter if we have no tangible goods to ship
			#
			if ($bCartHasTangibleGoods)
				{
				return ($::FAILURE, ACTINIC::GetPhrase(-1, 196));
				}
			}
		}
	else
		{
		#
		# We have a valid region code so lets get the region name
		#
		$::g_ShipContact{'ADDRESS4'} = $$::g_pLocationList{$::g_LocationInfo{DELIVERY_REGION_CODE}}{'NAME'};
		}

	if ($::g_LocationInfo{INVOICE_REGION_CODE} eq $ActinicOrder::UNDEFINED_REGION)
		{
		#
		# Region is undefined, check if need it
		#
		if (ActinicOrder::StateRequiredForValidation('Invoice', $::g_LocationInfo{INVOICE_COUNTRY_CODE}))
			{
			return ($::FAILURE, ACTINIC::GetPhrase(-1, 196));
			}
		}
	else
		{
		#
		# We have a valid region code so lets get the region name
		#
		$::g_BillContact{'ADDRESS4'} = $$::g_pLocationList{$::g_LocationInfo{INVOICE_REGION_CODE}}{'NAME'};
		}
	#
	# Check address field lengths
	#
	my $sError = main::CheckBothAddressesFieldLengths();
	my $nStatus = (length $sError < 1) ? $::SUCCESS : $::FAILURE;
	return ($nStatus, $sError);
	}

#######################################################
#
# GetShippingAddress - Get the shipping addres details
#
# Input: 0 - address hash
#			1 - true if using our shipping address
#
# Affects $::g_ShipContact
#
#######################################################

sub GetShippingAddress
	{
	my $hShipAddress = shift;
	my $bUseOurShippingAddress = shift;
	#
	# If we supplied the shipping address then
	# retain the fields not returned by PayPal
	#
	if (!$bUseOurShippingAddress)
		{
		main::ClearAddress(\%::g_ShipContact);
		#
		# Address fields not returned by PayPal
		#
		$::g_ShipContact{'PRIVACY'} 		= $::TRUE;
		}
	#
	# We have to translate GB to UK to be ISO compatible
	#
	$hShipAddress->{'address'}{'country_code'} =~ s/^GB$/UK/;
	#
	# Fetch the delivery address
	#
	$::g_ShipContact{'NAME'}			= GetIfDifferent($::g_ShipContact{'NAME'}, $hShipAddress->{'name'}{'full_name'});
	$::g_ShipContact{'ADDRESS1'}		= GetIfDifferent($::g_ShipContact{'ADDRESS1'}, $hShipAddress->{'address'}{'address_line_1'});
	$::g_ShipContact{'ADDRESS2'}		= GetIfDifferent($::g_ShipContact{'ADDRESS2'}, $hShipAddress->{'address'}{'address_line_2'});
	$::g_ShipContact{'ADDRESS3'}		= GetIfDifferent($::g_ShipContact{'ADDRESS3'}, $hShipAddress->{'address'}{'admin_area_2'});
	$::g_ShipContact{'POSTALCODE'}	= GetIfDifferent($::g_ShipContact{'POSTALCODE'}, $hShipAddress->{'address'}{'postal_code'});
	#
	# Keep ADDRESS4 as it was if using our own shipping address
	#
	if (!$bUseOurShippingAddress)
		{
		#
		# ADDRESS4 needs special handling as PayPal may return a state code
		# Attempt to use the returned admin_area_1 as a state code
		#
		my $sStateCode = sprintf("%s.%s", $hShipAddress->{'address'}{'country_code'}, uc($hShipAddress->{'address'}{'admin_area_1'}));
		my $sState = ActinicLocations::GetAddressRegionName($sStateCode);
		if ($sState eq '')
			{
			#
			# Not a valid state code so assume state name
			#
			$::g_ShipContact{'ADDRESS4'}		= GetIfDifferent($::g_ShipContact{'ADDRESS4'}, $hShipAddress->{'address'}{'admin_area_1'});
			}
		else
			{
			#
			# Valid state code so use the name of the state code
			#
			$::g_ShipContact{'ADDRESS4'}		= GetIfDifferent($::g_ShipContact{'ADDRESS4'}, $sState);
			}
		}
	#
	# Split name into first and last name if required
	#
	if ($$::g_pSetupBlob{'SHOPPER_NAME_HANDLING_MODE'} eq 1)	# first name/ last name handling
		{
		($::g_ShipContact{'FIRSTNAME'} , $::g_ShipContact{'LASTNAME'}) = ActinicOrder::SplitCustomerName($::g_ShipContact{'NAME'}); 
		}
	#
	# If simple tax and simple shipping then we cannot translate the code to a country so just use the code
	#
	$::g_LocationInfo{'DELIVERY_COUNTRY_CODE'} = $hShipAddress->{'address'}{'country_code'};
	$::g_ShipContact{'COUNTRY'}	= (ACTINIC::GetCountryName($::g_LocationInfo{'DELIVERY_COUNTRY_CODE'}) ne "") ?
												ACTINIC::GetCountryName($::g_LocationInfo{'DELIVERY_COUNTRY_CODE'}) : $::g_LocationInfo{'DELIVERY_COUNTRY_CODE'};
	}

#######################################################
#
# GetBillingAddress - Get the billing addres details
#
# Input:	0 - address hash
#			1 - true if using our billing address
#			2 - true if using our shipping address
#
# Affects $::g_BillContact
#
#######################################################

sub GetBillingAddress
	{
	my $hBillAddress = shift;
	my $bUseOurBillingAddress = shift;
	my $bUseOurShippingAddress = shift;
	#
	# If we supplied the billing address then
	# use it rather than the address from PayPal
	#
	if ($bUseOurBillingAddress)
		{
		return;
		}
	main::ClearAddress(\%::g_BillContact);
	#
	# Copy the shipping address to the billing address
	# The Terms and Conditions flag is stored in the Bill Contact hash
	# When the bill address is copied to the shipping address the flag is copied
	# The flag in the shipping contact is never cleared so would be copied
	# back by the following CopyHash and erroneously cause the T&C flag to be ticked
	# without any buyer action.
	# This problem applies to several fields.
	# Solution is to selectively copy the address using @ActinicOrder::arrAddressKeys
	#
	if ($bUseOurShippingAddress)
		{
		foreach (@ActinicOrder::arrAddressKeys)
			{
			$::g_BillContact{$_} = $::g_ShipContact{$_};
			}
		}
	#
	# Address fields not returned by PayPal
	#
	$::g_BillContact{'MOVING'} 		= $::FALSE;
	$::g_BillContact{'PRIVACY'} 		= $::TRUE;
	$::g_BillContact{'SEPARATE'} = $::g_LocationInfo{'SEPARATESHIP'} = $::FALSE;
	#
	# We have to translate GB to UK to be ISO compatible
	#
	$hBillAddress->{'address'}{'country_code'} =~ s/^GB$/UK/;
	#
	# Fetch the billing address
	#
	$::g_BillContact{'FIRSTNAME'}		= GetIfDifferent($::g_BillContact{'FIRSTNAME'}, $hBillAddress->{'name'}{'given_name'});
	$::g_BillContact{'LASTNAME'}		= GetIfDifferent($::g_BillContact{'LASTNAME'}, $hBillAddress->{'name'}{'surname'});
	$::g_BillContact{'EMAIL'}			= GetIfDifferent($::g_BillContact{'EMAIL'}, $hBillAddress->{'email_address'});
	#
	# Phone Type may be one of FAX, HOME, MOBILE, OTHER, PAGER
	#
	my $sPhoneType = $hBillAddress->{'phone'}{'phone_type'};
	if ('MOBILE' eq $sPhoneType)
		{
		LogData('PayPal::CollectContactDetails: Have a mobile number');
		$::g_BillContact{'MOBILE'}		= GetIfDifferent($::g_BillContact{'MOBILE'}, $hBillAddress->{'phone'}{'phone_number'}{'national_number'}); 
		}
	elsif (('HOME' eq $sPhoneType) ||
			 ('OTHER' eq $sPhoneType))
		{
		LogData('PayPal::CollectContactDetails: Have a home or other number');
		$::g_BillContact{'PHONE'}		= GetIfDifferent($::g_BillContact{'PHONE'}, $hBillAddress->{'phone'}{'phone_number'}{'national_number'});
		}
	elsif ('FAX' eq $sPhoneType)
		{
		LogData('PayPal::CollectContactDetails: Have a fax number');
		$::g_BillContact{'FAX'}		= GetIfDifferent($::g_BillContact{'FAX'}, $hBillAddress->{'phone'}{'phone_number'}{'national_number'});
		}

	$::g_BillContact{'NAME'}			= $::g_BillContact{'FIRSTNAME'} . ' ' . $::g_BillContact{'LASTNAME'};
	#
	# Having concatenated first/last names remove any leading or
	# trailing white space which may occur if either field was empty
	#
	$::g_BillContact{'NAME'}=~ s/^\s+|\s+$//g;
	#
	# If simple tax and simple shipping then we cannot translate the code to a country so just use the code
	#
	$::g_LocationInfo{'INVOICE_COUNTRY_CODE'} = $hBillAddress->{'address'}{'country_code'};
	$::g_BillContact{'COUNTRY'}	= (ACTINIC::GetCountryName($::g_LocationInfo{'INVOICE_COUNTRY_CODE'}) ne "") ?
												ACTINIC::GetCountryName($::g_LocationInfo{'INVOICE_COUNTRY_CODE'}) : $::g_LocationInfo{'INVOICE_COUNTRY_CODE'};
	}

#######################################################
#
# GetIfDifferent - Compares case insensitive 2 strings
#						 Returns original if no difference
#						 Returns the second string if different
#
# Input: 0 - Original string
#			1 - new string
#
# Returns:	0 - string
#
#######################################################

sub GetIfDifferent
	{
	my ($sOriginal, $sNew) = @_;
	if (uc($sOriginal) ne uc($sNew))
		{
		$sOriginal = $sNew;
		}
	return ($sOriginal);
	}

###############################################################
#
# SetFinalOrderDetails - Sets the final PayPal order details
#
# Input:	0 - PayPal order details hash
#
# Returns:	0 - status
#				1 - error message
#				2 - updated order details
#
###############################################################

sub SetFinalOrderDetails
	{
	my $hOrderDetails = shift;
	my $sOrderId = $hOrderDetails->{'id'};
	LogData('PayPal::SetFinalOrderDetails: started');
	#
	# Record the current order total
	#
	$::g_PaymentInfo{'ORDERTOTAL'} = ActinicOrder::GetOrderTotal($::TRUE);
	#
	# The PayPal Order status must be CREATED or APPROVED
	# but we shall determine first if we need to patch the order
	#
	my ($nStatus, $sError, $sOrderNumber) = main::GetOrderNumber();
	if ($nStatus != $::SUCCESS)
		{
		return ($nStatus, $sError);
		}

	my @aPatchParams = ();

	if (($hOrderDetails->{'purchase_units'}[0]{'amount'}{'value'} != BaseUnitsToCurrency($::g_PaymentInfo{'ORDERTOTAL'})) ||
		 ($hOrderDetails->{'purchase_units'}[0]{'amount'}{'currency_code'} ne GetOrderCurrencyCode()))
		{
		LogData(sprintf("PayPal::SetFinalOrderDetails: Changing amount/currency to %s %s", BaseUnitsToCurrency($::g_PaymentInfo{'ORDERTOTAL'}), GetOrderCurrencyCode()));
		#
		# Get the cart details
		# We shall get the total discount rather than showing as a
		# separate line as PayPal does not allow negative line items
		#
		my ($paItems, $nItemTotal, $nTotalDiscount);
		($nStatus, $sError, $paItems, $nItemTotal, $nTotalDiscount) = AddOrderLineItems(GetOrderCurrencyCode($::FALSE));
		#
		# To change the amount we have to replace the entire purchase_units array
		#
		# Set the current order total and currency
		#
		my %hPurchaseUnits;
		$hPurchaseUnits{'amount'} =
			{
			'currency_code' => GetOrderCurrencyCode(),
			'value' => BaseUnitsToCurrency($::g_PaymentInfo{'ORDERTOTAL'}),
			'breakdown' => 
				{'item_total' =>
					{
					'currency_code' => GetOrderCurrencyCode(),
					'value' => BaseUnitsToCurrency($nItemTotal)
					}
				}
			};
		#
		# Set the invoice/order number
		#
		$hPurchaseUnits{'invoice_id'} = $sOrderNumber;
		#
		# Get the ISO shipping country code
		#
		my $sShipCountryCode = ActinicLocations::GetISODeliveryCountryCode();
		#
		# Get the shipping state/province
		# use the region code (state code) for United States
		#
		my $sShipStateProvince = ($sShipCountryCode eq 'US') ? ActinicLocations::GetISODeliveryRegionCode() : $::g_ShipContact{'ADDRESS4'};
		#
		# PayPal ignores the county unless uppercase and matches their list
		# PayPal rejects the post code if not uppercase
		#
		$hPurchaseUnits{'shipping'} =
			{
			'name' =>
				{
				'full_name'	=> $::g_ShipContact{'NAME'}
				},
			'address' =>
				{
				'address_line_1'	=> $::g_ShipContact{'ADDRESS1'},
				'address_line_2'	=> $::g_ShipContact{'ADDRESS2'},
				'admin_area_2'		=> $::g_ShipContact{'ADDRESS3'},
				'admin_area_1'		=> uc($sShipStateProvince),
				'postal_code'		=> uc($::g_ShipContact{'POSTALCODE'}),
				'country_code'		=> $sShipCountryCode
				}
			};

		LogData('PayPal::SetFinalOrderDetails: Updated Shipping Address');
		$hPurchaseUnits{'items'} = $paItems;
		#
		# Add the discount if not zero
		#
		if ($nTotalDiscount)
			{
			$hPurchaseUnits{'amount'}{'breakdown'}{'discount'}{'currency_code'} = GetOrderCurrencyCode();
			$hPurchaseUnits{'amount'}{'breakdown'}{'discount'}{'value'} = BaseUnitsToCurrency($nTotalDiscount);
			}

		my @aUnits;
		$aUnits[0] = \%hPurchaseUnits;
		push (@aPatchParams,
			{
			'op' => 'replace',
			'path' => '/purchase_units/@reference_id==\'default\'',
			'value' => \%hPurchaseUnits
			});
		LogData("PayPal::SetFinalOrderDetails: Added cart to $sOrderNumber");
		}
	else
		{
		#
		# Patch only the invoice/order number if changed
		#
		if ($hOrderDetails->{'purchase_units'}[0]{'invoice_id'} ne $sOrderNumber)
			{
			my $sInvoiceIdAction = (defined $hOrderDetails->{'purchase_units'}[0]{'invoice_id'}) ? 'replace' : 'add';
			#
			# Set the current sales order number
			#
			LogData("PayPal::SetFinalOrderDetails: Setting invoice_id to $sOrderNumber");
			push (@aPatchParams,
						{
						'op' => $sInvoiceIdAction,
						'path' => '/purchase_units/@reference_id==\'default\'/invoice_id',
						'value' => $sOrderNumber
						});
			}
		}
	#
	# If we have any patch params then apply them
	#
	if (@aPatchParams)
		{
		#
		# The PayPal Order status must be CREATED or APPROVED
		#
		if (($hOrderDetails->{'status'} ne 'CREATED') &&
			 ($hOrderDetails->{'status'} ne 'APPROVED'))
			{
			RecordErrors(sprintf("PayPal::SetFinalOrderDetails: Unexpected order status %s for PayPal order id %s", $hOrderDetails->{'status'}, $hOrderDetails->{'id'}));
			return ($::FAILURE, $PayPal::PAYPAL_ERROR);
			}
		LogData('PayPal::SetFinalOrderDetails: applying changes');
		my ($nStatus, $sError) = SendApiRequest('checkout/orders/' . $sOrderId, 'PATCH', \@aPatchParams);
		if ($nStatus != $::SUCCESS)
			{
			return ($nStatus, $sError);
			}
		#
		# We know what was changed so just update and return the order
		# details hash without re-getting the order as that takes time
		#
		$hOrderDetails->{'purchase_units'}[0]{'amount'}{'value'} = BaseUnitsToCurrency($::g_PaymentInfo{'ORDERTOTAL'});
		$hOrderDetails->{'purchase_units'}[0]{'amount'}{'currency_code'} = GetOrderCurrencyCode();
		$hOrderDetails->{'purchase_units'}[0]{'invoice_id'} = $sOrderNumber;
		}
	else
		{
		LogData('PayPal::SetFinalOrderDetails: no changes required');
		}

	LogData('PayPal::SetFinalOrderDetails: finished successfully');

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

#######################################################
#
# GetOrderCurrencyCode - Get the order currency code
#
#	Returns:	0 - the currency code
#
#######################################################

sub GetOrderCurrencyCode
	{
	#
	# Get the order currency
	#
	return ($$::g_pCatalogBlob{SINTLSYMBOLS});
	}

#######################################################
#
# CurrencyToBaseUnits - Convert currency value such as
#								12.34 to base value 1234
#
#	Input:	0 - currency value
#
#	Returns:	0 - base value
#
#######################################################

sub CurrencyToBaseUnits
	{
	my $nValue = shift;
	#
	# Get the amount in base units (pence/cents etc.)
	#
	return ($nValue * (10 ** $$::g_pCatalogBlob{"ICURRDIGITS"}));
	}

#######################################################
#
# BaseUnitsToCurrency - Convert internal value to float
#								1234 to float value 12.34
#
#	Input:	0 - internal value
#
#	Returns:	0 - base value
#
#######################################################

sub BaseUnitsToCurrency
	{
	my $nValue = shift;
	#
	# Get the amount as a float n.nn
	#
	return ($nValue / (10 ** $$::g_pCatalogBlob{"ICURRDIGITS"}));
	}

###############################################################
#
# RecordPayPalAuthorization - Record the PayPal authorizationn
#
# Input:		0 - PayPal order Id
#				1 - Authorization ID
#				2 - 3DS Enrolled Yes/No (optional)
#				3 - 3DS Result (optional)
#
# Returns:	0 - status
#				1 - error message
#
###############################################################

sub RecordPayPalAuthorization
	{
	my $sOrderId = shift;
	my $sAuthorisationId = shift;
	my $s3DsEnrolled = shift;
	my $s3DsResult = shift;
	#
	# Get the Authorization, do not simply rely on the details returned
	# in the Authorize creation as they may have already changed
	#
	my ($nStatus, $sError, $hAuthDetails) = SendApiRequest('payments/authorizations/' . $sAuthorisationId, 'GET');
	if ($nStatus != $::SUCCESS)
		{
		return($::FAILURE, $sError);
		}
	my $sAuthorizationState = $hAuthDetails->{'status'};

	if (($sAuthorizationState eq 'CREATED') ||
		 ($sAuthorizationState eq 'CAPTURED') ||
		 ($sAuthorizationState eq 'PARTIALLY_CAPTURED') ||
		 ($sAuthorizationState eq 'VOIDED') ||
		 ($sAuthorizationState eq 'PENDING'))
		{
		#
		# Get the PayPal order
		#
		my ($pOrderDetails);
		($nStatus, $sError, $pOrderDetails) = GetResource('up', $hAuthDetails->{'links'});
		if ($nStatus != $::SUCCESS)
			{
			return ($nStatus, $sError);
			}
		#
		# Create an OCC file for these authorisation states
		#
		($nStatus, $sError) = RecordAuthorization(GetParamsFromAuthorization($sOrderId, $hAuthDetails),
										GetCardDetails($pOrderDetails->{'payment_source'}), $s3DsEnrolled, $s3DsResult);
		if ($nStatus != $::SUCCESS)
			{
			return($::FAILURE, $sError);
			}
		}
	elsif (($sAuthorizationState eq 'DENIED') ||
		 ($sAuthorizationState eq 'EXPIRED'))
		{
		#
		# Do not create an OCC file for these authorisation states
		#
		LogData("PayPal::RecordPayPalAuthorization: Authorisation failed status $sAuthorizationState for order ID $sOrderId");
		return($::FAILURE, $PayPal::PAYPAL_ERROR);
		}
	else
		{
		#
		# Fail on unexpected authorisation states
		#
		RecordErrors("PayPal::RecordPayPalAuthorization: Receieved unexpected authorisation status $sAuthorizationState for order ID $sOrderId");
		return($::FAILURE, $PayPal::PAYPAL_ERROR);
		}
	#
	# Finished successfully
	#
	return($::SUCCESS);
	}

###############################################################
#
# RecordPayPalCapture - Record the PayPal capture
#
# Input:		0 - PayPal order Id
#				1 - Capture ID
#				2 - 3DS Enrolled Yes/No (optional)
#				3 - 3DS Result (optional)
#
# Returns:	0 - status
#				1 - error message
#
###############################################################

sub RecordPayPalCapture
	{
	my $sOrderId = shift;
	my $sCaptureId = shift;
	my $s3DsEnrolled = shift;
	my $s3DsResult = shift;
	#
	# Get the Authorization, do not simply rely on the details returned
	# in the Authorize creation as they may have already changed
	#
	my ($nStatus, $sError, $hCaptureDetails) = SendApiRequest('payments/captures/' . $sCaptureId, 'GET');
	if ($nStatus != $::SUCCESS)
		{
		return($::FAILURE, $sError);
		}
	my $sCaptureState = $hCaptureDetails->{'status'};

	if (($sCaptureState eq 'COMPLETED') ||
		 ($sCaptureState eq 'PENDING') ||
		 ($sCaptureState eq 'REFUNDED'))
		{
		#
		# Get the PayPal order
		#
		my ($pOrderDetails);
		($nStatus, $sError, $pOrderDetails) = GetPayPalOrder($sOrderId);
		if ($nStatus != $::SUCCESS)
			{
			return ($nStatus, $sError);
			}
		#
		# Create an OCC file for these capture states
		#
		($nStatus, $sError) = RecordAuthorization(GetParamsFromCapture($sOrderId, $hCaptureDetails, ''),
										GetCardDetails($pOrderDetails->{'payment_source'}), $s3DsEnrolled, $s3DsResult);
		if ($nStatus != $::SUCCESS)
			{
			return($::FAILURE, $sError);
			}
		}
	elsif ($sCaptureState eq 'DECLINED')
		{
		#
		# Do not create an OCC file for DECLINED
		#
		LogData("PayPal::RecordPayPalCapture: Capture failed status $sCaptureState for order ID $sOrderId");
		return ($::FAILURE, 'Your payment has been declined. ' . $PayPal::RETRYMSG);
		}
	elsif ($sCaptureState eq 'PARTIALLY_REFUNDED')
		{
		#
		# Do not create an OCC file for PARTIALLY_REFUNDED
		#
		LogData("PayPal::RecordPayPalCapture: Capture failed status $sCaptureState for order ID $sOrderId");
		return($::FAILURE, $PayPal::PAYPAL_ERROR);
		}
	else
		{
		#
		# Fail on unexpected capture states
		#
		RecordErrors("PayPal::RecordPayPalCapture: Receieved unexpected capture status $sCaptureState for order ID $sOrderId");
		return($::FAILURE, $PayPal::PAYPAL_ERROR);
		}
	#
	# Finished successfully
	#
	return($::SUCCESS);
	}

#######################################################
#
# GetParamsFromAuthorization - Get params
#			from a PayPal Authorization object
#
# IMPORTANT: The list of returned parameters must match
# the input parameters for RecordAuthorization
#
# Params:	0 - PayPal Order Id
#				1 - PayPal Authorization hash
#
# Returns:	0 - PayPal Authorization ID
#				1 - Sellerdeck Order Number
#				2 - Capture ID, only if captured
#				3 - Authorised amount
#				4 - Authorised currency
#				5 - 0=>Capture, 1=>Pre-Authorise, 2=>Refund, 3=>Void
#				6 - PayPal order ID
#				7 - Creation time stamp
#				8 - Payment Status
#				9 - Comment
#			  10 - Processor Response hash
#
#######################################################

sub GetParamsFromAuthorization
	{
	my $sOrderId = shift;
	my $hAuthDetails = shift;

	my $sAuthorizationState = $hAuthDetails->{'status'};

	my $nPaymentType = 1;								# assume pre-authorise
	my $nPaymentStatus = $::PSP_STATUS_PreAuth;

	if	(($sAuthorizationState eq 'CAPTURED') ||
		 ($sAuthorizationState eq 'PARTIALLY_CAPTURED'))
		{
		$nPaymentType = 0;
		}
	elsif ($sAuthorizationState eq 'VOIDED')
		{
		$nPaymentType = 3;
		}

	return(	$hAuthDetails->{'id'},
				$hAuthDetails->{'invoice_id'},
				undef,
				$hAuthDetails->{'amount'}{'value'},
				$hAuthDetails->{'amount'}{'currency_code'},
				$nPaymentType,
				$sOrderId,
				$hAuthDetails->{'update_time'},
				$nPaymentStatus,
				GetComment($hAuthDetails),
				$hAuthDetails->{'processor_response'});
	}

#######################################################
#
# GetParamsFromCapture - Get params
#			from a PayPal Capture object
#
# IMPORTANT: The list of returned parameters must match
# the input parameters for RecordAuthorization
#
# Params:	0 - PayPal Order Id
#				1 - PayPal Capture hash
#				2 - Authorization Id
#
# Returns:	0 - PayPal Authorization ID (only for intent AUTHORIZE)
#				1 - Sellerdeck Order Number
#				2 - Capture ID, only if captured
#				3 - Authorised amount
#				4 - Authorised currency
#				5 - 0=>Capture, 1=>Pre-Authorise, 2=>Refund, 3=>Void
#				6 - PayPal order ID
#				7 - Creation time stamp
#				8 - Payment Status
#				9 - Comment
#			  10 - Processor Response hash
#
#######################################################

sub GetParamsFromCapture
	{
	my $sOrderId = shift;
	my $hCaptureDetails = shift;
	my $sPayPalAuthorizationId = shift;

	my $sCaptureState = $hCaptureDetails->{'status'};

	my $nPaymentType = 0;								# assume capture
	my $nPaymentStatus = $::PSP_STATUS_Accepted;	# assume accepted

	if ($sCaptureState eq 'PENDING')
		{
		$nPaymentStatus = $::PSP_STATUS_OCCPending;
		}
	elsif ($sCaptureState eq 'REFUNDED')
		{
		$nPaymentType = 2;
		}
	elsif (($sCaptureState eq 'VOIDED') ||
			 ($sCaptureState eq 'DECLINED'))			# declined is equivalent to voiding
		{
		$nPaymentType = 3;
		}

	return(	$sPayPalAuthorizationId,
				$hCaptureDetails->{'invoice_id'},
				$hCaptureDetails->{'id'},
				$hCaptureDetails->{'amount'}{'value'},
				$hCaptureDetails->{'amount'}{'currency_code'},
				$nPaymentType,
				$sOrderId,
				$hCaptureDetails->{'update_time'},
				$nPaymentStatus,
				GetComment($hCaptureDetails),
				$hCaptureDetails->{'processor_response'});
	}

#######################################################
#
# GetParamsFromRefund - Get params
#			from a PayPal Refund object
#
# IMPORTANT: The list of returned parameters must match
# the input parameters for RecordAuthorization
#
# Params:	0 - PayPal Order Id
#				1 - PayPal Refund hash
#
# Returns:	0 - PayPal Authorization ID (only for intent AUTHORIZE)
#				1 - Sellerdeck Order Number
#				2 - Capture ID, only if captured
#				3 - Authorised amount
#				4 - Authorised currency
#				5 - 0=>Capture, 1=>Pre-Authorise, 2=>Refund, 3=>Void
#				6 - PayPal order ID
#				7 - Creation time stamp
#				8 - Payment Status
#				9 - undef
#			  10 - undef
#
#######################################################

sub GetParamsFromRefund
	{
	my $sOrderId = shift;
	my $hRefundDetails = shift;

	my $nPaymentStatus = ($hRefundDetails->{'status'} eq 'COMPLETED') ? $::PSP_STATUS_PaymentSent : $::PSP_STATUS_Rejected;

	return(	undef,
				$hRefundDetails->{'invoice_id'},
				$hRefundDetails->{'id'},
				$hRefundDetails->{'amount'}{'value'},
				$hRefundDetails->{'amount'}{'currency_code'},
				2,
				$sOrderId,
				$hRefundDetails->{'update_time'},
				$nPaymentStatus,
				undef,
				undef);
	}

#######################################################
#
# GetParamsFromVoid - Get params
#			from a PayPal Void object
#
# IMPORTANT: The list of returned parameters must match
# the input parameters for RecordAuthorization
#
# Params:	0 - PayPal Order Id
#				1 - PayPal Void hash
#
# Returns:	0 - PayPal Authorization ID (only for intent AUTHORIZE)
#				1 - Sellerdeck Order Number
#				2 - Capture ID, only if captured
#				3 - Authorised amount
#				4 - Authorised currency
#				5 - 0=>Capture, 1=>Pre-Authorise, 2=>Refund, 3=>Void
#				6 - PayPal order ID
#				7 - Creation time stamp
#				8 - Payment Status
#				9 - undef
#			  10 - undef
#
#######################################################

sub GetParamsFromVoid
	{
	my $sOrderId = shift;
	my $hVoidDetails = shift;

	return(	undef,
				$hVoidDetails->{'invoice_id'},
				$hVoidDetails->{'id'},
				$hVoidDetails->{'amount'}{'value'},
				$hVoidDetails->{'amount'}{'currency_code'},
				3,
				$sOrderId,
				$hVoidDetails->{'update_time'},
				$::PSP_STATUS_PreAuth,
				undef,
				undef);
	}


#######################################################
#
# GetComment - Constructs a comment for the payment reord
#
# Params:	0 - payment hash (Authorize or Capture
#
# Returns:	0 - comment or undef
#
#######################################################

sub GetComment
	{
	my $phPayment = shift;
	my $sSellerProtection = $phPayment->{'seller_protection'}{'status'};
	if (length $sSellerProtection)
		{
		return (sprintf("Seller Protection: %s", $sSellerProtection));
		}
	return (undef);
	}

#######################################################
#
# GetCardDetails - Constructs a card description
#
# Params:	0 - payment_source hash (Authorize or Capture
#
# Returns:	0 - card description or undef
#
#######################################################

sub GetCardDetails
	{
	my $phPaymentSource = shift;

	if (defined $phPaymentSource)
		{
		return(sprintf("%s *%s",
			$phPaymentSource->{'card'}{'brand'},
			$phPaymentSource->{'card'}{'last_digits'}));
		}

	return (undef);
	}

#######################################################
#
# RecordAuthorization - implements PayPal specific
#									payment authorization
#
# Params:	0 - PayPal Authorization ID
#				1 - Sellerdeck Order Number
#				2 - Capture ID, only if captured
#				3 - Authorised amount
#				4 - Authorised currency
#				5 - 0=>Capture, 1=>Pre-Authorise, 2=>Refund, 3=>Void
#				6 - PayPal order ID
#				7 - Creation time stamp
#				8 - Payment Status
#				9 - Comment
#			  10 - Processor Response hash
#			  11 - Card description (or undef)
#			  12 - 3DS Enrolled Yes/No (optional)
#			  13 - 3DS Result (optional)
#
# Returns:	0 - $::SUCCESS or $::FAILURE
#				1 - undef or error message
#
#######################################################

sub RecordAuthorization
	{
	my $sPayPalAuthorizationId = shift;
	my $sOrderNumber = shift;
	my $sCaptureId = shift;
	my $nAmount = shift;
	my $sCurrency = shift;
	my $nPaymentType = shift;
	my $sPayPalOrderId = shift;
	my $sCreationTimestamp = shift;
	my $nPaymentStatus = shift;
	my $sComment = shift;
	my $phProcessorResponse = shift;
	my $sCardDescription = shift;
	my $bPreAuthorize = ($nPaymentType == 1) ? $::TRUE : $::FALSE;
	my $s3DsEnrolled = shift;
	my $s3DsResult = shift;

	my $sError;

	LogData(sprintf("PayPal::RecordAuthorization: %s/%s %s", $sPayPalOrderId, $sPayPalAuthorizationId, $sCreationTimestamp));
	#
	# Make sure the currency is as expected
	#
	if (!IsSameCurrency($sCurrency))
		{
		$sError = sprintf("Authorisation not recorded because of currency mismatch. Received %s but expected %s for order %s", $sCurrency, GetOrderCurrencyCode(), $sOrderNumber);
		if (IsIpnRequest())
			{
			#
			# We only return FAILURE if this is an IPN (webhook)
			# This is because the order and payment have been taken
			# it is just that we could not record the payment occ file
			#
			return($::FAILURE, $sError);
			}
		else
			{
			RecordErrors('PayPal::RecordAuthorization: ' . $sError);
			return($::SUCCESS, '');
			}
		}
	#
	# Get the authorised amount in base units (pence/cents etc.)
	#
	$nAmount = CurrencyToBaseUnits($nAmount);
	#
	# Record the authorization
	#
	my $sAction = $::g_InputHash{ACTION};			# the auth function uses this so we need to override but create a backup first
	$::g_InputHash{ON} = $sOrderNumber;
	$::g_InputHash{TM} = $PayPal::TESTMODE;
	$::g_InputHash{PA} = $bPreAuthorize;
	$::g_InputHash{AM} = $nAmount;
	#
	# If we are using an Authorize session file then
	# this should be treated as a normal authorization
	#
	# Important:
	# We always set the payment method here to be PayPal
	# and not PayPal (Cards). This is to simplify the
	# offline payment button processing
	#
	if ((defined $::Session) &&
		 ($::Session->IsAuthorisationFile()))
		{
		$::g_InputHash{ACTION} = sprintf("AUTHORIZE_%d", $::PAYMENT_PAYPALCHECKOUT);
		}
	else
		{
		#
		# We may have a session file but it is not an authorize session
		#
		$::g_InputHash{ACTION} = sprintf("OFFLINE_AUTHORIZE_%d", $::PAYMENT_PAYPALCHECKOUT);
		}
	#
	# Build the parameters for the auth record
	#
	my $sDate = GetActinicDateFromISO8601Date($sCreationTimestamp);

	($sDate) = ACTINIC::EncodeText2($sDate, $::FALSE);
	#
	# Override the Payment Type using PT param
	# Translate internal Payment Type to desktop Payment Type
	#
	my $sPaymentType;
	if ($nPaymentType == 0)
		{
		$sPaymentType = "&PT=0";						# Capture
		}
	elsif ($nPaymentType == 1)
		{
		$sPaymentType = "&PT=10";						# Authorize
		}
	elsif ($nPaymentType == 2)
		{
		$sPaymentType = "&PT=1";						# Refund
		}
	elsif ($nPaymentType == 3)
		{
		$sPaymentType = "&PT=12";						# Void
		}
	if (0 < length $sComment)
		{
		$sComment = "&CT=$sComment";					# Comment
		}
	if (0 < length $sCaptureId)
		{
		$sCaptureId = "&PR=$sCaptureId";				# PSP Response
		}
	if (0 < length $sPayPalOrderId)
		{
		$sPayPalOrderId = "&TX=$sPayPalOrderId";	# PSP Provider Ref
		}
	#
	# Always set CD param so we avoid a null code in the DB
	# as the desktop code only looks for "" and not for null
	#
	$sPayPalAuthorizationId = "&CD=$sPayPalAuthorizationId";

	my $sParams = sprintf("ON=%s%s&TM=%s&AM=%s&%s%s%s&DT=%s&PA=%s&PS=%s%s&",
		$sOrderNumber,
		$sPaymentType,
		$PayPal::TESTMODE,
		$nAmount,
		$sPayPalOrderId,
		$sCaptureId,
		$sPayPalAuthorizationId,
		$sDate,
		$bPreAuthorize,
		$nPaymentStatus.
		$sComment);

	if (defined $phProcessorResponse)
		{
		#
		# CVV verification
		#
		if (($phProcessorResponse->{'cvv_code'} eq 'M') ||
			 ($phProcessorResponse->{'cvv_code'} eq '0'))
			{
			$sParams .= sprintf("RC=%s&", 'Matched');
			}
		elsif (($phProcessorResponse->{'cvv_code'} eq 'N') ||
			 ($phProcessorResponse->{'cvv_code'} eq '1'))
			{
			$sParams .= sprintf("RC=%s&", 'Not Matched');
			}
		#
		# Address verification (Maestro)
		#
		if ($phProcessorResponse->{'avs_code'} eq '0')
			{
			$sParams .= sprintf("RA=%s&", 'Matched');
			}
		if ($phProcessorResponse->{'avs_code'} eq '1')
			{
			$sParams .= sprintf("RA=%s&", 'Not Matched');
			}
		if ($phProcessorResponse->{'avs_code'} eq '2')
			{
			$sParams .= sprintf("RA=%s&", 'Partial Match');
			}
		#
		# Address verification (Not Maestro)
		#
		if (($phProcessorResponse->{'avs_code'} eq 'A') ||
			 ($phProcessorResponse->{'avs_code'} eq 'B'))
			{
			$sParams .= sprintf("RA=%s&", 'Matched');
			$sParams .= sprintf("ZR=%s&", 'Not Matched');
			}
		elsif (($phProcessorResponse->{'avs_code'} eq 'C') ||
			 ($phProcessorResponse->{'avs_code'} eq 'N'))
			{
			$sParams .= sprintf("RA=%s&", 'Not Matched');
			$sParams .= sprintf("ZR=%s&", 'Not Matched');
			}
		elsif (($phProcessorResponse->{'avs_code'} eq 'D') ||
			 ($phProcessorResponse->{'avs_code'} eq 'F') ||
			 ($phProcessorResponse->{'avs_code'} eq 'M') ||
			 ($phProcessorResponse->{'avs_code'} eq 'X') ||
			 ($phProcessorResponse->{'avs_code'} eq 'Y'))
			{
			$sParams .= sprintf("RA=%s&", 'Matched');
			$sParams .= sprintf("ZR=%s&", 'Matched');
			}
	
		elsif (($phProcessorResponse->{'avs_code'} eq 'P') ||
			 ($phProcessorResponse->{'avs_code'} eq 'W') ||
			 ($phProcessorResponse->{'avs_code'} eq 'Z'))
			{
			$sParams .= sprintf("RA=%s&", 'Not Matched');
			$sParams .= sprintf("ZR=%s&", 'Matched');
			}
		}
	if (defined $s3DsEnrolled)
		{
		$sParams .= sprintf("3E=%s&", $s3DsEnrolled);
		}
	if (defined $s3DsResult)
		{
		$sParams .= sprintf("3R=%s&", $s3DsResult);
		}
	if (defined $sCardDescription)
		{
		$sParams .= sprintf("CF=%s&", $sCardDescription);
		}
	#
	# Create the signature
	#
	my $sSignature = ACTINIC::GetMD5Hash($sParams);
	$sParams .= sprintf("SN=%s", $sSignature);
	#
	# Record the authorization and submits to MailChimp if required
	#
	LogData(sprintf("PayPal::RecordAuthorization: calling RecordAuthorization with %s", $sParams));
	$sError = main::RecordAuthorization(\$sParams);
	$::g_InputHash{ACTION} = $sAction;				# restore the original action
	if (length $sError)
		{
		#
		# Record any error to error.err
		#
		RecordErrors(sprintf("PayPal::RecordAuthorization: RecordAuthorization returned  %s", $sError));
		if (IsIpnRequest())
			{
			#
			# We only return FAILURE if this is an IPN (webhook)
			# This is because the order and payment have been taken
			# it is just that we could not record the payment occ file
			#
			return ($::FAILURE, ACTINIC::GetPhrase(-1, 1964));
			}
		}
	else
		{
		LogData(sprintf("PayPal::RecordAuthorization: finished successfully"));
		}

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

#######################################################
#
# GetActinicDateFromISO8601Date - Gets the Actinic date/time
#												from ISO 8601 format
#
#		This is a wrapper for ACTINIC::GetActinicDateFromISO8601Date
#		which expects a UTC date/time so this makes the date
#		a UTC date before passing to ACTINIC::GetActinicDateFromISO8601Date
#
# Params:	0 - ISO-8601 formatted date
#
# Returns:	0 - Date in Actinic format
#
#######################################################

sub GetActinicDateFromISO8601Date
	{
	my $sIso8601Date = shift;
	#
	# If already a UTC date/time then pass directly to ACTINIC::GetActinicDateFromISO8601Date
	#
	if ($sIso8601Date =~ /Z$/)
		{
		return (ACTINIC::GetActinicDateFromISO8601Date($sIso8601Date));
		}
 	eval
		{
		require HTTP::Date;								# Try loading HTTP::Date
		import HTTP::Date 'str2time';
		import HTTP::Date 'time2str';
		};
	if ($@)
		{
		LogData("GetActinicDateFromISO8601Date: Module HTTP::Date not found");
		#
		# We don't have HTTP::Date so use the passed date without the offset from UTC
		#
		$sIso8601Date =~ s/[-|+]\d{2}:\d{2}$/Z/;
		return (ACTINIC::GetActinicDateFromISO8601Date($sIso8601Date));
		}
	#
	# Convert ASCII date/time to machine time and pass to GetISO8601Date
	#
	$sIso8601Date = ACTINIC::GetISO8601Date(str2time($sIso8601Date));
	#
	# Convert UTC date/time to Actinic date/time
	#
	return (ACTINIC::GetActinicDateFromISO8601Date($sIso8601Date));
	}

#######################################################
#
# SaveSessionAndSendResponse - Save session file and
#											send the JSON response
#
#	Input: 	0 - JSON response as a hash
#
#######################################################

sub SaveSessionAndSendResponse
	{
	my $sResponse = shift;
	#
	# Ensure checkout data is saved
	#
	main::UpdateCheckoutRecord();
	#
	# Make sure the session is saved
	#
	$::Session->SaveSession();
	my $sJsonResponse = ACTINIC::EncodeJson($sResponse);
	LogData("PayPal::SaveSessionAndSendResponse: Response is $sJsonResponse");
	ACTINIC::PrintJSON($sJsonResponse);
	}

###############################################################
#
#  AddOrderLineItems - Adds hash of order lines to array
#
#	Input:	0 - Order currency
#				1 - record discounts as lines (default true)
#
#	Returns:	0 - $::SUCCESS or $::FAILURE
#				1 - Error message if $::FAILURE
#				2 - array of item hashes
#				3 - item total
#				4 - discount total (+ve value)
#
###############################################################

sub AddOrderLineItems
	{
	my $sCurrency = shift;
	my $bDiscountLines = shift;

	if (!defined $bDiscountLines)
		{
		$bDiscountLines = $::TRUE;
		}

	my $nTotalDiscount = 0;								# discount in internal format
	my $nTotalItems = 0;									# items in internal format

	my @arrItems;

	my ($nStatus, $sError, $pCartObject);
	#
	# Process and add line items
	#
	($nStatus, $sError, $pCartObject) = $::Session->GetCartObject();
	if ($nStatus != $::SUCCESS)
		{
		return ($nStatus, $sError);
		}
	#
	# Summarize the order, get shipping, handling and tax information
	#
	my ($nStatus, $sError, $nSubTotal, $nShipping, $nTax1, $nTax2, $nTotal,
			$nShippingTax1, $nShippingTax2, $nHandling, $nHandlingTax1, $nHandlingTax2)
			 = $pCartObject->SummarizeOrder($::TRUE);
	if ($nStatus != $::SUCCESS)
		{
		return ($nStatus, $sError);
		}

	my $pCartList = $pCartObject->GetCartList();
	my ($pOrderDetail, $pProduct);
	my ($sSectionBlobName);
	my ($nProductPrice, $nPrice, $rarrCurTaxBands, $rarrDefTaxBands, @aProductTax);
	#
	# Now iterate through the individual orderlines
	#
	my ($parrAdjustments, $parrAdjustDetails);
	my $nLine = 0;
	foreach $pOrderDetail (@$pCartList)
		{		
		my ($sSectionBlobName);
		($nStatus, $sError, $sSectionBlobName) = ACTINIC::GetSectionBlobName($$pOrderDetail{SID}); # retrieve the blob name
		if ($nStatus == $::FAILURE)
			{
			return ($sError);
			}
		#
		# locate this product's object.
		#
		($nStatus, $sError, $pProduct) = ACTINIC::GetProduct($$pOrderDetail{"PRODUCT_REFERENCE"}, $sSectionBlobName,
												  ACTINIC::GetPath());	# get this product object
		if ($nStatus != $::SUCCESS)					# the item has been removed from the catalog or other error occurred
			{
			return ($::FAILURE, $sError);
			}

		($nStatus, $sError, $nProductPrice, $nPrice,
			$rarrCurTaxBands, $rarrDefTaxBands,	@aProductTax) = $pCartObject->GetCartItemPrice($pOrderDetail);
		if ($nStatus != $::SUCCESS)					# the item has been removed from the catalog or other error occurred
			{
			return ($::FAILURE, $sError);
			}
		my $sDescription = '';
		my $sName = $$pProduct{'NAME'};
		#
		# Collect variants in the cart if any
		#
		if ($pProduct->{'COMPONENTS'})				# For all components/attributes of the product
			{
			my $lstVariants = ActinicOrder::GetCartVariantList($pOrderDetail);
			my ($c, %Component);
			foreach $c (@{$pProduct->{'COMPONENTS'}})
				{
				($nStatus, %Component) = ActinicOrder::FindComponent($c, $lstVariants);				
				if ($nStatus != $::SUCCESS)
					{
					return ($nStatus, $Component{'text'}, "", "");
					}
				my $sCompDesc = ($Component{'text'} ne "") ? $Component{'text'} : $c->[$::CBIDX_NAME];
				$sDescription .= (($sDescription ne "") ? ", " : "") . $sCompDesc;				
				}			
			}
		push (@arrItems, OrderItemHash($sName, $$pOrderDetail{'PRODUCT_REFERENCE'}, $sDescription, $$pOrderDetail{'QUANTITY'}, $nPrice, $sCurrency));
		$nTotalItems += $$pOrderDetail{'QUANTITY'} * $nPrice;
		#
		# Add adjustments as orderlines in order to get the totals right
		#			
		$parrAdjustments = $pCartObject->GetProductAdjustments($nLine);		
		foreach $parrAdjustDetails (@$parrAdjustments)
			{
			my $nAmount = $parrAdjustDetails->[$::eAdjIdxAmount];
			if (($nAmount > 0) &&
				 ($bDiscountLines))
				{
				push (@arrItems, OrderItemHash($parrAdjustDetails->[$::eAdjIdxProductDescription], '', '', 1, $nAmount, $sCurrency));
				$nTotalItems += $nAmount;
				}
			else
				{
				$nTotalDiscount += $nAmount;
				}
			}
		$nLine++;
		}	
	#
	# Add any order adjustments
	#
	my $nOrderAdjustments = 0;
	$parrAdjustments = $pCartObject->GetOrderAdjustments();	
	push @$parrAdjustments, @{$pCartObject->GetFinalAdjustments()};
	foreach $parrAdjustDetails (@$parrAdjustments)
		{
		my $nAmount = $parrAdjustDetails->[$::eAdjIdxAmount];
		if (($nAmount > 0) &&
			 ($bDiscountLines))
			{
			push (@arrItems, OrderItemHash($parrAdjustDetails->[$::eAdjIdxProductDescription], '', '', 1, $nAmount, $sCurrency));
			$nTotalItems += $nAmount;
			}
		else
			{
			$nTotalDiscount += $nAmount;
			}
		}
	# 
	# Add order totals
	#
	my $nTaxTotal = 0;
	if (!ActinicOrder::PricesIncludeTaxes())		# no tax passed on if order lines already including tax
		{
		$nTaxTotal = $nTax1 + $nTax2;
		}
	elsif (($nTax1 + $nTax2) < 0)
		{
		#
		# Negative tax, that means we have to subtract it from the total
		# PP don't accept negative values so all we can do is to drop it
		# into the discount total
		#
		$nTotalDiscount += $nTax1 + $nTax2;
		}
	if ($nTaxTotal > 0)
		{
		if ($nTax1 > 0)
			{
			push (@arrItems, OrderItemHash(ActinicOrder::GetTaxName('TAX_1'), '', '', 1, $nTax1, $sCurrency));
			$nTotalItems += $nTax1;
			}
		if ($nTax2 > 0)
			{
			push (@arrItems, OrderItemHash(ActinicOrder::GetTaxName('TAX_2'), '', '', 1, $nTax2, $sCurrency));
			$nTotalItems += $nTax2;
			}
		}
	#
	# Add shipping charge if applied
	#
	if ($$::g_pSetupBlob{'MAKE_SHIPPING_CHARGE'} &&
		($nShipping != 0))
		{
		my @Response = ActinicOrder::GetShippingPluginResponse(); # get the shipping description
		my $sShipDescription = '';						# Set default in case of errors
		if ($Response[0] == $::SUCCESS)
			{
			if (${$Response[2]}{GetShippingDescription} == $::SUCCESS)
				{
				$sShipDescription = $Response[5];
	         }
			}
		push (@arrItems, OrderItemHash('Shipping', '', $sShipDescription, 1, $nShipping, $sCurrency));
		$nTotalItems += $nShipping;
		}
	#
	# Add handling charge if applied
	#
	if ($$::g_pSetupBlob{'MAKE_HANDLING_CHARGE'} &&
		($nHandling != 0))
		{
		push (@arrItems, OrderItemHash(ACTINIC::GetPhrase(-1, 199) . ":", '', '', 1, $nHandling, $sCurrency));
		$nTotalItems += $nHandling;
		}
	return ($::SUCCESS, '', \@arrItems, $nTotalItems, abs($nTotalDiscount));
	}

#######################################################
#
# OrderItemHash - Create an order item hash
#
# Params:	0 - item type (sku, shipping, tax)i
#				1 - sku (product reference)
#				2 - description of item
#				3 - ordered quantity
#				4 - unit price
#				5 - currency code
#
# Returns:	0 - hash of order item details
#
#######################################################

sub OrderItemHash
	{
	my ($sType, $sSku, $sDescription, $nQuantity, $nPrice, $sCurrency) = @_;
	my $sShortType = $sType;

	if (length $sType > 127)
		{
		$sShortType = substr($sType, 0, 124) . '...';
		}
	my (%hItem) =	(	'name' => $sShortType,
							'quantity' => $nQuantity,
							'unit_amount' => 
								{
								'currency_code' => $sCurrency,
								'value' => BaseUnitsToCurrency($nPrice)
								}
							);
	#
	# Show product reference (sku) if required and not empty string
	#
	if ((length $sSku) &&
		 ($$::g_pSetupBlob{PROD_REF_COUNT} > 0))
		{
		$hItem{'sku'} = $sSku;
		}
	#
	# Include description if not an empty string, but limit to 127 characters total
	#
	my $nLength = length $sDescription;
	if ($nLength)
		{
		$hItem{'description'} = ($nLength > 127) ? substr($sDescription, 0, 124) . '...' : $sDescription;
		}

	return(\%hItem);
	}

#######################################################
#
# LoadAuthoriseFile - Attempts to load the authorise file
#
# For IPNs the authorise file may not exist
#
# Params:	0 - Sellerdeck order number
#
# Returns:	0 - status
#				1 - error if any
#
#######################################################

sub LoadAuthoriseFile
	{
	my $sOrderNumber = shift;
	#
	# Get the Authorization ID from the authorise file if present
	#
	if (main::IsAuthoriseSessionPresent($sOrderNumber))
		{
		#
		# We have an authorization file
		#
		my ($sContactDetails);						# place holder to keep Session constructor happy
		$::Session = new Session($sOrderNumber, $sContactDetails, ACTINIC::GetPath(), $::FALSE, $::TRUE, $::TRUE);
		if (!$::Session->IsAuthorisationFile())
			{
			#
			# We need the authorisation file to be able to complete this procedure
			#
			return($::FAILURE, 'Session file is not an authorise file');
			}
		#
		# Get the checkout details from the session file
		# This will load a number of global hashes
		#
		return(main::GetCheckoutStatus());
		}
	return($::FAILURE, "Cannot find authorise file for $sOrderNumber");
	}

######################################################################
#
# GetCachedToken	- gets a cached PayPal token
#
# Returns	0 -	token or undef if failed
#
######################################################################

sub GetCachedToken
	{
	#
	# Use the cached token if we already got one
	#
	if ((defined $PayPal::TOKEN) &&
		 ($PayPal::TOKEN ne ''))
		{
		LogData("PayPal::GetCachedToken: Using cached token");
		return ($PayPal::TOKEN);
		}
	#
	# If we don't have a session object (webhook) then just get a new token
	#
	if (!defined $::Session)
		{
		LogData("PayPal::GetCachedToken: No session so creating a new token");
		$PayPal::TOKEN = GetToken();
		return ($PayPal::TOKEN);
		}
	#
	# Get the current token, if any, from the session file
	#
	$PayPal::TOKEN = $::Session->GetProviderParam($::PAYMENT_PAYPALCHECKOUT, $::PAYPAL_ACCESS_TOKEN);
	my $nExpiryTime = $::Session->GetProviderParam($::PAYMENT_PAYPALCHECKOUT, $::PAYPAL_EXPIRY_TIME);
	if ((length($PayPal::TOKEN) > 0) &&
		 ($::Session->GetProviderParam($::PAYMENT_PAYPALCHECKOUT, $::PAYPAL_RESOURCE) eq $PayPal::PROCESS_SCRIPT_HOST) &&
		 (time < $nExpiryTime))
		{
		#
		# Use the stored token if it is for the same resource and hasn't expired
		#
		LogData("PayPal::GetCachedToken: Using stored token");
		return ($PayPal::TOKEN);
		}
	#
	# Otherwise get a new token
	#
	LogData("PayPal::GetCachedToken: No current valid token so creating a new token");
	$PayPal::TOKEN = GetToken();
	return ($PayPal::TOKEN);
	}

######################################################################
#
# GetToken	- gets a PayPal token
#
# Returns	0 -	token or undef if failed
#
######################################################################

sub GetToken
	{
	#
	# Get a secure connection
	#
	my $sCredentials = Base64Encode(sprintf("%s:%s", $PayPal::PSP_CLIENT_ID, $PayPal::PSP_SECRET_KEY));
	my $SSLConnection =  SSLConnection->new($PayPal::PROCESS_SCRIPT_HOST, $PayPal::PORT, "/v1/oauth2/token");
	
	$SSLConnection->SetRequestMethod('POST');
	$SSLConnection->SetRequestTimeout($PayPal::HTTP_TIMEOUT);
	$SSLConnection->SetHeaderValue('Authorization', 'Basic ' . $sCredentials);
	$SSLConnection->SetHeaderValue('Accept', 'application/json');
	$SSLConnection->SetHeaderValue('Accept-Language', 'en_US');
	#
	# Setting the response error level to NONE so this method
	# must log any unexpected responses status codes
	#
	$SSLConnection->SetRequestErrorLevel($::HTTP_ERROR_LEVEL_NONE);
	#
	# We can re-issue this request without causing any problems
	# if the previous request completed but could not respond 
	#
	my $bRetry = $::TRUE;
	my $nRetries = $PayPal::HTTP_MAX_RETRIES;
	while ($bRetry)
		{
		$bRetry = $::FALSE;
		$SSLConnection->SendRequest('grant_type=client_credentials');
		if (($SSLConnection->GetResponseCode() == 408) ||
			 ($SSLConnection->GetResponseCode() > 499))
			{
			if ($nRetries)
				{
				$nRetries--;
				sleep $PayPal::HTTP_RETRY_INTERVAL;
				$bRetry = $::TRUE;
				}
			}
		}
	if (($SSLConnection->GetResponseCode() > 399) ||
		 ($SSLConnection->GetResponseCode() < 500))
		{
		#
		# The request was successful but the data is invalid
		# This can be a JSON error structure indicating the type of error
		#
		my $phResult = $SSLConnection->GetResponseJSON();
		if (defined $phResult->{'error'})
			{
			RecordErrors(sprintf("PayPal::GetToken: SendRequest returned (%s) %s",
					$phResult->{'error'},
					$phResult->{'error_description'}));
			return (undef);
			}
		}
	#
	# Due to the way the error handling was done connection status
	# also means not 200 OK so we need to check the response code first
	#
	if ($SSLConnection->GetConnectStatus() == $::FALSE)
		{
		RecordErrors(sprintf("PayPal::GetToken: SendRequest returned (%s) %s",
				$SSLConnection->GetResponseCode(),
				$SSLConnection->GetConnectErrorMessage()));
		return (undef);
		}

	my ($phResponse) = $SSLConnection->GetResponseJSON();
	#
	# If we don't have a session object (webhook) then we can't save the token
	#
	if (defined $::Session)
		{
		#
		# When does the token expire?
		# Would be more accurate to use the page header date as the basis of the calculation
		# but Date::Parse and DateTime are not core perl
		#
		my $nExpiryTime = time + $phResponse->{'expires_in'};	# expire  time
	
		$::Session->SetProviderParam($::PAYMENT_PAYPALCHECKOUT, $::PAYPAL_SCOPE, $phResponse->{'scope'});
		$::Session->SetProviderParam($::PAYMENT_PAYPALCHECKOUT, $::PAYPAL_ACCESS_TOKEN, $phResponse->{'access_token'});
		$::Session->SetProviderParam($::PAYMENT_PAYPALCHECKOUT, $::PAYPAL_TOKEN_TYPE, $phResponse->{'token_type'});
		$::Session->SetProviderParam($::PAYMENT_PAYPALCHECKOUT, $::PAYPAL_APP_ID, $phResponse->{'app_id'});
		$::Session->SetProviderParam($::PAYMENT_PAYPALCHECKOUT, $::PAYPAL_EXPIRY_TIME, $nExpiryTime);
		$::Session->SetProviderParam($::PAYMENT_PAYPALCHECKOUT, $::PAYPAL_NONCE, $phResponse->{'nonce'});
		$::Session->SetProviderParam($::PAYMENT_PAYPALCHECKOUT, $::PAYPAL_RESOURCE, $PayPal::PROCESS_SCRIPT_HOST);
		}
	return ($phResponse->{'access_token'});
	}

######################################################################
#
# GetCachedClientToken	- gets a cached PayPal client token for SDK URL
#
# Returns	0 -	client token or undef if failed
#
######################################################################

sub GetCachedClientToken
	{
	#
	# Get the current client token, if any, from the session file
	#
	my $sToken = $::Session->GetProviderParam($::PAYMENT_PAYPALCHECKOUT, $::PAYPAL_CLIENT_TOKEN);
	my $nExpiryTime = $::Session->GetProviderParam($::PAYMENT_PAYPALCHECKOUT, $::PAYPAL_CLIENT_EXPIRY_TIME);
	if ((length($sToken) > 0) &&
		 ($::Session->GetProviderParam($::PAYMENT_PAYPALCHECKOUT, $::PAYPAL_RESOURCE) eq $PayPal::PROCESS_SCRIPT_HOST) &&
		 (time < $nExpiryTime))
		{
		LogData("PayPal::GetCachedClientToken: Using stored token");
		#
		# Use the stored client token if it is for the same resource and hasn't expired
		#
		return ($sToken);
		}
	#
	# Otherwise get a new client token
	#
	LogData("PayPal::GetCachedClientToken: No current valid client token so creating a new client token");
	$sToken = GetClientToken();
	return ($sToken);
	}

######################################################################
#
# GetClientToken	- gets a PayPal Client Token
#							used for Custom Card Fields
#
# Returns	0 -	client token or undef if failed
#
######################################################################

sub GetClientToken
	{
	#
	# Get a secure connection
	#
	my $sCredentials = Base64Encode(sprintf("%s:%s", $PayPal::PSP_CLIENT_ID, $PayPal::PSP_SECRET_KEY));
	my $SSLConnection =  SSLConnection->new($PayPal::PROCESS_SCRIPT_HOST, $PayPal::PORT, "/v1/identity/generate-token");
	
	$SSLConnection->SetRequestMethod('POST');		# do not use GET as it returns 404
	$SSLConnection->SetRequestTimeout($PayPal::HTTP_TIMEOUT);
	$SSLConnection->SetHeaderValue('Authorization', 'Basic ' . $sCredentials);
	$SSLConnection->SetHeaderValue('Accept', 'application/json');
	$SSLConnection->SetHeaderValue('Accept-Language', 'en_US');
	$SSLConnection->SetHeaderValue('Content-Type', 'application/json');	# returns media error without this
	#
	# Setting the response error level to NONE so this method
	# must log any unexpected responses status codes
	#
	$SSLConnection->SetRequestErrorLevel($::HTTP_ERROR_LEVEL_NONE);
	#
	# We can re-issue this request without causing any problems
	# if the previous request completed but could not respond 
	#
	my $bRetry = $::TRUE;
	my $nRetries = $PayPal::HTTP_MAX_RETRIES;
	while ($bRetry)
		{
		$bRetry = $::FALSE;
		$SSLConnection->SendRequest();
		if (($SSLConnection->GetResponseCode() == 408) ||
			 ($SSLConnection->GetResponseCode() > 499))
			{
			if ($nRetries)
				{
				$nRetries--;
				sleep $PayPal::HTTP_RETRY_INTERVAL;
				$bRetry = $::TRUE;
				}
			}
		}

	if (($SSLConnection->GetResponseCode() > 399) ||
		 ($SSLConnection->GetResponseCode() < 500))
		{
		#
		# The request was successful but the data is invalid
		# This can be a JSON error structure indicating the type of error
		#
		my $phResult = $SSLConnection->GetResponseJSON();
		if (defined $phResult->{'error'})
			{
			RecordErrors(sprintf("PayPal::GetClientToken: SendRequest PayPal error (%s) %s",
					$phResult->{'error'},
					$phResult->{'error_description'}));
			return (undef);
			}
		}
	#
	# Due to the way the error handling was done connection status
	# also means not 200 OK so we need to check the response code first
	#
	my $sJsonResponse = $SSLConnection->GetResponseJSON();
	if ($SSLConnection->GetConnectStatus() == $::FALSE)
		{
		my $sDebugId;
		if (exists $sJsonResponse->{'debug_id'})
			{
			$sDebugId = sprintf(" Debug ID:%s", $sJsonResponse->{'debug_id'});
			}
		RecordErrors(sprintf("PayPal::GetClientToken: %s (%s)%s",
				$SSLConnection->GetConnectErrorMessage(),
				$SSLConnection->GetResponseCode(),
				$sDebugId));
		return (undef);
		}

	my $nExpiryTime = time + $sJsonResponse->{'expires_in'};	# expire  time

	$::Session->SetProviderParam($::PAYMENT_PAYPALCHECKOUT, $::PAYPAL_CLIENT_TOKEN, $sJsonResponse->{'client_token'});
	$::Session->SetProviderParam($::PAYMENT_PAYPALCHECKOUT, $::PAYPAL_CLIENT_EXPIRY_TIME, $nExpiryTime);
	$::Session->SetProviderParam($::PAYMENT_PAYPALCHECKOUT, $::PAYPAL_RESOURCE, $PayPal::PROCESS_SCRIPT_HOST);

	return ($sJsonResponse->{'client_token'});
	}

######################################################################
#
# SendApiRequest	- Send a request to the PayPal API server
#
# Input		0 -	REST request
#				1 -	Mode (GET/POST etc)
#				2 -	pointer to params - may be hash, array or scalar
#
# Returns	0 -	status $::SUCCESS or $::FAILURE
#				1 -	error message if not $::SUCCESS
#				2 -	response hash
#
######################################################################

sub SendApiRequest
	{
	my $sRequest = shift;
	my $sMode = shift;
	my $pParams = shift;

	return (SendRequest($PayPal::PROCESS_SCRIPT_HOST, $PayPal::PORT, "/v$PayPal::PSP_API_VERSION/$sRequest",
				$sMode, $pParams, GetIdempotencyId($sRequest, $sMode)));
	}

######################################################################
#
# SendWebHookRequest	- Send a request to the PayPal webhook server
#
# Input		0 -	REST request
#				1 -	Mode (GET/POST etc)
#				2 -	pointer to params - may be hash, array or scalar
#
# Returns	0 -	status $::SUCCESS or $::FAILURE
#				1 -	error message if not $::SUCCESS
#				2 -	response hash
#
######################################################################

sub SendWebHookRequest
	{
	my $sRequest = shift;
	my $sMode = shift;
	my $pParams = shift;

	return (SendRequest($PayPal::PROCESS_SCRIPT_HOST, $PayPal::PORT, "/v$PayPal::PSP_WEBHOOK_VERSION/$sRequest",
	$sMode, $pParams, GetIdempotencyId($sRequest, $sMode)));
	}

######################################################################
#
# GetIdempotencyId	- Get a unique reference for this request
#
# Input		0 -	REST request
#				1 -	Mode (GET/POST etc)
#
# Returns	0 -	idempotency id or undef if a GET request
#
######################################################################

sub GetIdempotencyId
	{
	my $sRequest = shift;
	my $sMode = shift;

	if (uc($sMode) eq 'GET')
		{
		return (undef);
		}
	#
	# Use idempotency except for GET requests
	# An MD5 of the request + time + random number should be unique
	#
	srand();
	return (ACTINIC::GetMD5Hash($sRequest . time() . rand()));
	}

######################################################################
#
# SendRequest	- Send a request to the PayPal server
#
# Input		0 -	REST request
#				1 -	Mode (GET/POST etc)
#				2 -	pointer to params - may be hash, array or scalar
#
# Returns	0 -	status $::SUCCESS or $::FAILURE
#				1 -	error message if not $::SUCCESS
#				2 -	response hash
#
######################################################################

sub SendRequest
	{
	my $sHost = shift;
	my $nPort = shift;
	my $sRequest = shift;
	my $sMode = shift;
	my $pParams = shift;
	my $sIdempotencyId = shift;
	#
	# Ignore Idempotency if it is not 32 characters (MD5 length)
	#
	if (length $sIdempotencyId != 32)
		{
		undef $sIdempotencyId;
		}
	#
	# Get the cached access token
	#
	my $sToken = GetCachedToken();
	if (!defined $sToken)
		{
		#
		# Could not get the access token
		#
		RecordErrors('PayPal::SendRequest: Could not get the access token');
		return ($::FAILURE, $PayPal::PAYPAL_ERROR);
		}
	#
	# Get a secure connection
	#
	my $SSLConnection =  SSLConnection->new($sHost, $nPort, $sRequest);
	
	$SSLConnection->SetRequestMethod($sMode);
	$SSLConnection->SetHeaderValue('Content-Type', 'application/json');
	$SSLConnection->SetHeaderValue("Authorization", "Bearer " . $sToken);
	$SSLConnection->SetHeaderValue("Prefer", "return=representation");
	if (defined $sIdempotencyId)
		{
		$SSLConnection->SetHeaderValue("PayPal-Request-Id", $sIdempotencyId);
		}
	if (length $PayPal::PSP_PARTNERID)
		{
		$SSLConnection->SetHeaderValue("PayPal-Partner-Attribution-Id", $PayPal::PSP_PARTNERID);
		}
	else
		{
		RecordErrors("Partner ID is not set");
		}
	$SSLConnection->SetRequestTimeout($PayPal::HTTP_TIMEOUT);
	#
	# Setting the response error level to NONE so this method
	# must log any unexpected responses status codes
	#
	$SSLConnection->SetRequestErrorLevel($::HTTP_ERROR_LEVEL_NONE);
	#
	# Get the parameters as a JSON string if defined
	# Params may be a HASH, ARRAY or string
	#
	my $sContent = "";
	if ((ref($pParams) eq 'HASH') ||
		 (ref($pParams) eq 'ARRAY'))
		{
		$sContent = (defined $pParams) ? ACTINIC::EncodeJson($pParams) : '';
		}
	elsif ($pParams ne '')
		{
		$sContent = $pParams;
		}

	my $bRetry = $::TRUE;
	my $bFetchedToken = $::FALSE;
	#
	# The request will re-use the last acquired token but if the token has
	# expired then a new token will be acquired and the request re-tried
	# If the request fails with a 408, 429 or 5** status then we shall retry the
	# request (max  HTTP_MAX_RETRIES) but only if a GET or idempotency is defined
	#
	my $bRetry = $::TRUE;
	my $nRetries = 0;
	if ((defined $sIdempotencyId) ||
		 (uc($sMode) eq 'GET'))
		{
		$nRetries = $PayPal::HTTP_MAX_RETRIES;
		}
	while ($bRetry)
		{
		$bRetry = $::FALSE;
		$SSLConnection->SendRequest($sContent);
		if (($SSLConnection->GetResponseCode() == 408) ||	# Request timeout
			 ($SSLConnection->GetResponseCode() == 429) ||	# PayPal rate exceeded
			 ($SSLConnection->GetResponseCode() > 499))		# Server error
			{
			if ($nRetries)
				{
				$nRetries--;
				LogData("PayPal::SendRequest: Retrying failed $sMode request (Idempotency ID:$sIdempotencyId)");
				sleep $PayPal::HTTP_RETRY_INTERVAL;
				$bRetry = $::TRUE;
				next;
				}
			}
		#
		# Due to the way the error handling was done connection status
		# also means not 200 OK so we need to check the response code first
		#
		if ($SSLConnection->GetResponseCode() == 401)
			{
			#
			# No valid API key provided. GetToken() should be called
			# but we shall only do this once
			#
			if (!$bFetchedToken)
				{
				$SSLConnection->SetHeaderValue("Authorization", "Bearer " . GetToken());
				$bFetchedToken = $::TRUE;
				$bRetry = $::TRUE;						# retry irrespective of max retries
				next;
				}
			RecordErrors('PayPal::SendRequest: ' . $SSLConnection->GetConnectErrorMessage());
			return ($::FAILURE, $PayPal::PAYPAL_ERROR);
			}
		}

	my $sJsonResponse = $SSLConnection->GetResponseJSON();
	if (($SSLConnection->GetResponseCode() == 422) &&
		 ($sJsonResponse->{'name'} eq 'UNPROCESSABLE_ENTITY'))
		{
		#
		# We have some processing or business error
		# Construct a message for the buyer
		#
		my $sMessage;
		my $paDetails = $sJsonResponse->{'details'};
		foreach my $hDetail (@{$paDetails})
			{
			LogData('PayPal::SendRequest: ' . $hDetail->{'issue'});
			if ($hDetail->{'issue'} eq 'PAYER_ACTION_REQUIRED')
				{
				return ($::FAILURE, $hDetail->{'issue'});
				}
			if ($hDetail->{'issue'} eq 'INSTRUMENT_DECLINED')
				{
				$sMessage = 'Your payment has been declined. ' . $PayPal::RETRYMSG;
				return ($::FAILURE, $sMessage);
				}
			if ($hDetail->{'issue'} eq 'DUPLICATE_INVOICE_ID')
				{
				RecordErrors(sprintf("Duplicate Invoice ID for order %s.\r\nRequest: %s\r\nResponse: %s",
							$::g_PaymentInfo{'ORDERNUMBER'}, $sRequest, $SSLConnection->GetResponseContent()));
				$sMessage = sprintf('%s quoting your order number %s', ACTINIC::GetPhrase(-1, 1964), $::g_PaymentInfo{'ORDERNUMBER'});
				return ($::FAILURE, $sMessage);
				}
			if ($hDetail->{'issue'} eq 'ORDER_ALREADY_CAPTURED')
				{
				RecordErrors(sprintf("Payment already captured for order %s.\r\nRequest: %s\r\nResponse: %s",
							$::g_PaymentInfo{'ORDERNUMBER'}, $sRequest, $SSLConnection->GetResponseContent()));
				$sMessage = sprintf('%s quoting your order number %s', ACTINIC::GetPhrase(-1, 1964), $::g_PaymentInfo{'ORDERNUMBER'});
				return ($::FAILURE, $sMessage);
				}
			if ($hDetail->{'issue'} eq 'ORDER_ALREADY_AUTHORIZED')
				{
				RecordErrors(sprintf("Payment already authorized for order %s.\r\nRequest: %s\r\nResponse: %s",
							$::g_PaymentInfo{'ORDERNUMBER'}, $sRequest, $SSLConnection->GetResponseContent()));
				$sMessage = sprintf('%s quoting your order number %s', ACTINIC::GetPhrase(-1, 1964), $::g_PaymentInfo{'ORDERNUMBER'});
				return ($::FAILURE, $sMessage);
				}
			}
		}

	if ($SSLConnection->GetResponseCode() > 299)
		{
		#
		# All 2xx codes are okay but not 3xx, 4xx or 5xx
		# Include the PayPal Debug ID if available
		#
		my $sDebugId;
		if (exists $sJsonResponse->{'debug_id'})
			{
			$sDebugId = sprintf(" Debug ID:%s", $sJsonResponse->{'debug_id'});
			}
		RecordErrors(sprintf("PayPal::SendRequest: %s (%s)%s",
				$SSLConnection->GetConnectErrorMessage(),
				$SSLConnection->GetResponseCode(),
				$sDebugId));
		return ($::FAILURE, $PayPal::PAYPAL_ERROR);
		}
	return ($::SUCCESS, "", $sJsonResponse);
	}

#######################################################
#																		
# ReadPostData - read the posted data.  It is still in
#   the queue because the Actinic code only expects
#   GET or POST data, but not both and it handles GET
#   first.
#
# Expects:  $::ENV{CONTENT_LENGTH} to be defined
#           STDIN to contain the POST data
#
# Returns:	0 - status
#				1 - error if any
#				2 - reference to hash containing the parameters
#           3 - the raw posted data string
#
#######################################################

sub ReadPostData
	{
#
# Uncoment this code if you need to know the
# PayPal headers received with a POST from PayPal
#
#	my $var;
#	foreach $var (sort(keys(%ENV))) 					# iterate ENV variables
#		{														# add all as a new row
#		if ($var =~ /HTTP_PAYPAL_/)
#			{
#			LogData("$var = " . $ENV{$var});
#			}
#		}
	my $sHeaderSignature = $ENV{'HTTP_PAYPAL_TRANSMISSION_SIG'};
	my $sHeaderAlgorithm = $ENV{'HTTP_PAYPAL_AUTH_ALGO'};
	my $sHeaderCertUrl = $ENV{'HTTP_PAYPAL_CERT_URL'};
	my $sHeaderId = $ENV{'HTTP_PAYPAL_TRANSMISSION_ID'};
	my $sHeaderTime = $ENV{'HTTP_PAYPAL_TRANSMISSION_TIME'};
	#
	# Ensure the request has a signature
	#
	if (length $sHeaderSignature < 1)
		{
		return ($::FAILURE, 'PayPal::ReadPostData: Ignoring request without signature in header');
		}
	#
	# As of SDD v18.2.4, ACTINIC::ReadAndParseInoput will read the POST
	# data and store itto the global variable $::g_PostInputData
	#
	my $InputData = $::g_PostInputData;

	LogData('PayPal::ReadPostData: Input data is ' . $InputData);
	my ($bOk, $sError, $phJson) = ACTINIC::ParseJsonResponse('', $InputData);
	my $nStatus = $bOk ? $::SUCCESS : $::FAILURE;
	if ($nStatus == $::SUCCESS)
		{
		#
		# The verification requires that the webhook event fields are
		# passed in the same order in which they are received so we
		# cannot use the JSON hash as the field order will change
		#
		my $hResponse;
		my $sRequest = sprintf('{"auth_algo":"%s","cert_url":"%s","transmission_id":"%s","transmission_sig":"%s","transmission_time":"%s","webhook_id":"%s","webhook_event":%s}',
											$sHeaderAlgorithm,
											$sHeaderCertUrl,
											$sHeaderId,
											$sHeaderSignature,
											$sHeaderTime,
											$PayPal::PSP_WEBHOOKID,
											$InputData);
		utf8::encode($sRequest);
		($nStatus, $sError, $hResponse) = SendWebHookRequest('notifications/verify-webhook-signature', 'POST', $sRequest);
		if ($nStatus != $::SUCCESS)
			{
			return ($nStatus, $sError);
			}
		if ($hResponse->{'verification_status'} ne 'SUCCESS')
			{
			$sError = 'Webhook verification failed';
			LogData("PayPal::ReadPostData: $sError");
			return ($::FAILURE, $sError);
			}
		}

	return ($nStatus, $sError, $phJson);
	}

#######################################################
#
# Base64Encode - Encode a string with Base 64 encoding
#
#	Input: 	0 - string to encode
#
#	Returns:	1 - Base 64 encoded string
#
#######################################################

sub Base64Encode ($;$)
	{
	my $res = "";
	my $eol = $_[1];
	$eol = "\n" unless defined $eol;
	pos($_[0]) = 0;                       		   # ensure start at the beginning
	
	$res = join '', map( pack('u',$_)=~ /^.(\S*)/, ($_[0]=~/(.{1,45})/gs));
	
	$res =~ tr|` -_|AA-Za-z0-9+/|;               # `# help emacs
	# fix padding at the end
	my $padding = (3 - length($_[0]) % 3) % 3;
	$res =~ s/.{$padding}$/'=' x $padding/e if $padding;
	return $res;
	}

#######################################################
#
# RecordErrors - Wrapper for the RecordErrors in ACTINIC.pm
#				Pre-pends web hook message id if present
#
#	Input: 	0 - message to log
#
#######################################################

sub RecordErrors
	{
	my $sMessage = shift;
	if ($PayPal::WEBHOOKMESSAGEID ne '')
		{
		$sMessage = $PayPal::WEBHOOKMESSAGEID . '->' . $sMessage;
		}
	ACTINIC::RecordErrors($sMessage);
	}

#######################################################
#
# GetSdkQuery - Get additional SDK query params
#						This may be called from ShoppingCart.pl
#
#	Input:		0 - PAGETYPE, type of page being rendered
#					1 - URL PayPal SDK URL to be modified
#
#	Returns: 	0 - the additional query string
#
#######################################################

sub GetSdkQuery
	{
	my $sPageType = shift;
	my $sUrl = shift;
	LogData("PayPal::GetSdkQuery: Page is '$sPageType'");
	#
	# Split the URL into the left of the string upto and aincluding the '?'
	# and the right string, everything after the '?'
	#
	$sUrl =~ /(.*?\?)(.*)/;
	my $sLeft = $1;										# URL up to and including '?'
	my $sRight = $2;										# query parameters

	my $sQuery = sprintf("intent=%s&currency=%s&", lc($PayPal::INTENT), GetOrderCurrencyCode());

	if ($PayPal::TESTMODE)
		{
		$sQuery .= 'debug=true&';
		my $sBillCountryCode = ActinicLocations::GetISOInvoiceCountryCode();
		if ($sBillCountryCode ne '')
			{
			$sQuery .= sprintf('buyer-country=%s&', $sBillCountryCode);
			}
		}
	my $bPaymentPage = ($sPageType eq 'Checkout Page 2') ? $::TRUE : $::FALSE;
	#
	# Determine the disabled fundings
	#
	my @aDisabledFundings;
	if (!$PayPal::PSP_CREDIT_ALLOWED)
		{
		#
		# PayPal Credit not allowed
		#
		LogData('PayPal::GetSdkQuery: Credit not allowed');
		push (@aDisabledFundings, 'credit');
		}
	if (!$PayPal::PSP_APMS_ALLOWED ||
		 !$bPaymentPage)
		{
		#
		# APMs only allowed on payment page
		#
		LogData('PayPal::GetSdkQuery: APMs not allowed');
		push (@aDisabledFundings, @PayPal::APM_FUNDING_LIST);
		}
	#
	# Cards are only enabled if a client token is required
	#
	my $bCardsEnabled = ($sRight =~ /your-client-token/) ? $::TRUE : $::FALSE;

	if ($bCardsEnabled)
		{
		#
		# Get the client token
		#
		my $sClientToken = GetCachedClientToken();
		$sRight =~ s/your-client-token/$sClientToken/;	# inject the client token
		}
	else
		{
		#
		# Cards not allowed
		#
		LogData('PayPal::GetSdkQuery: Cards not allowed');
		push (@aDisabledFundings, 'card');
		}
	#
	# Get a comma separated list of disabled fundings
	#
	if (@aDisabledFundings)
		{
		$sQuery .= sprintf('disable-funding=%s&', join(',', @aDisabledFundings));
		}

	$sUrl = $sLeft . $sQuery . $sRight;
	LogData("PayPal::GetSdkQuery: SDK URL is $sUrl");
	return ($sUrl);
	}

#######################################################
#
# LogData - Wrapper for the LogData in OrderScript.pl
#				Pre-pends web hook message id if present
#
#	Input: 	0 - message to log
#
#######################################################

sub LogData
	{
	my $sMessage = shift;
	if ($PayPal::WEBHOOKMESSAGEID ne '')
		{
		$sMessage = $PayPal::WEBHOOKMESSAGEID . '->' . $sMessage;
		}
	ACTINIC::LogData($sMessage, $::DC_ORDERSCRIPT);
	}

#######################################################
#
# END OF PayPal PACKAGE
#
#######################################################
#
# Must be at the end as any following code will not be parsed
#
return ($::SUCCESS);
#
# End of File
