#--------------------------------------------------------------- # # OCCSagePay.pl - code part of OCC script # # Copyright (c) SellerDeck Limited 2001 All rights reserved # # *** Do not change this code unless you know what you are doing *** # # Written by George Menyhert # Adapted for PROTX VPS Version 2.2 by Mat Peck - 27/05/2002 # Includes simple XOR encryption and Base64 encode functions # # This script is called by an eval() function and it will already # have the following variables set up: # # Expects: $::sOrderNumber - the alphanumeric order number for this order # $::nOrderTotal - the total for this order (stored in based currency format e.g. 1000 = $10.00) # %::PriceFormatBlob - the price format data # %::InvoiceContact - the customer invoice contact information # %::OCCShipData - the customer delivery contact information # $::sCallBackURLAuth - the URL of the authorization callback script # $::sCallBackURLBack - the URL of the backup script # $::sCallBackURLUser - the URL of the receipt script # $::sPath - the path to the Catalog directory # $::sWebSiteUrl - the Catalog web site URL # $::sContentUrl - the content URL # # Affects: $::eStatus - the status of the transaction: # $::FAILURE - Failure # $::ACCEPTED - Accepted # $::REJECTED - Rejected # $::PENDING - Pending # $::sErrorMessage - error message if any # $::sHTML - the HTML to display # # $Revision: 38216 $ # #--------------------------------------------------------------- use strict; ###################################################################### # Sage Pay VPS Specific constants here. We use global variables to # persist the details beyond the OCCPlugin call ###################################################################### $::eStatus = $::PENDING; # The OCC plug-in runs in pending mode. This script does not # perform the transaction. Rather, it forwards the customer to # the OCC site for completion. $::PSP_MERCHANT_ID = ACTINIC::DecryptPspParam($sADF01); $::PSP_KEY = ACTINIC::DecryptPspParam($sADF02); $::PSP_PASSWORD = "sADF03"; $::PSP_CONFIRMATION_EMAIL = ""; # (sADF04) $::PSP_INCONTEXTPSP = "0"; # 0=>No, 1=>Yes (sADF05) $::PSP_USESAUTHORISEFILE = $::TRUE; $::PSP_API_PATH = "/api/1.0/merchant-session-key/"; $::PSP_DIRECT_PATH = "/gateway/service/vspdirect-register.vsp"; $::PSP_3DSECURE_PATH = "/gateway/service/direct3dcallback.vsp"; $::PSP_LIB_URL = "/api/1.0/js/sagepay.js"; $::PSP_SECURE_SERVER_PORT = 443; $::PSP_HOST = ""; if ($bTestMode) { $::PSP_HOST = "sandbox.opayo.eu.elavon.com"; } else { $::PSP_HOST = "live.opayo.eu.elavon.com"; } $::PSP_AUTHORISE_URL = $::sCallBackURLAuth; $::PSP_AUTHORISE_URL =~ s/\?.*$//; if (IsInContextPSP()) { ###################################################################### # # Sage Pay API # The following is executed only when called by CallOCCPlugin # ###################################################################### if ($::g_InputHash{ACTION} eq 'GETPSPFORM') { # # Get the In Context form for this PSP # ($::eStatus, $::sErrorMessage, $::sHTML) = GetInContextForm(); } if ($::g_InputHash{ACTION} eq 'INCONTEXTPSP') { if (defined $::g_InputHash{'3DAUTH'}) { # # Handle the 3DSecure response # ($::eStatus, $::sErrorMessage, $::sHTML) = Get3DSecurePayment(); } else { # # Get the In Context Payment Hash # ($::eStatus, $::sErrorMessage, $::sHTML) = GetPSPPayment(); } } } else { if ($::g_InputHash{ACTION} eq 'GETPSPFORM') { # # Get the Server form HTML for this PSP # ($::eStatus, $::sErrorMessage, $::sHTML) = RegisterServerTransaction(); } elsif ($::g_InputHash{ACTION} eq 'INCONTEXTPSP') { # # Get the In Context Payment Hash # ($::eStatus, $::sErrorMessage, $::sHTML) = RecordServerPayment(); exit; # the method should not have returned here } return ($::SUCCESS); } ###################################################################### # # IsInContextPSP - Indicates if this is an In Context PSP # # Returns $::TRUE if In Context # ###################################################################### sub IsInContextPSP { return ($::PSP_INCONTEXTPSP == 1 ? $::TRUE : $::FALSE); } ###################################################################### # # UseAuthoriseFile - Does this PSP use the authorise file? # # Returns: $::TRUE if PSP uses the authorise file # ###################################################################### sub UseAuthoriseFile { return ($::PSP_USESAUTHORISEFILE); } ###################################################################### # # GetInContextForm - Prepare the CC details form # # Returns 0 - status $::SUCCESS or $::FAILURE # 1 - error message if not $::SUCCESS # 2 - HTML of the form # ###################################################################### sub GetInContextForm { # # Fetch the Merchant key from Sage Pay # my ($Status, $sMessage, $sMerchantkey) = GetMerchantKey(); if ($Status != $::SUCCESS) { return ($::FAILURE, $sMessage); } my $sLibraryUrl = sprintf("https://%s%s", $::PSP_HOST, $::PSP_LIB_URL); # # Using double quotes around end tag so variables are iterpolated # my $sHTML = <<"END_HTML";

Payment Card Details

Close Form
END_HTML return ($::SUCCESS, "", $sHTML); } ###################################################################### # # GetMerchantKey - Fetch the merchant key from Sage Pay # # Returns 0 - status $::SUCCESS or $::FAILURE # 1 - error message if not $::SUCCESS # 2 - merchant key # ###################################################################### sub GetMerchantKey { # # Get a secure connection # my $SSLConnection = SSLConnection->new($::PSP_HOST, $::PSP_SECURE_SERVER_PORT, $::PSP_API_PATH); $SSLConnection->SetRequestMethod("POST"); $SSLConnection->SetHeaderValue("Authorization", "Basic " . Base64Encode("$::PSP_KEY:$::PSP_PASSWORD")); $SSLConnection->SetHeaderValue("Content-Type", "application/json"); $SSLConnection->SetRequestTimeout(5); # set timeout to 2 seconds my %sParams = ("vendorName" => $::PSP_MERCHANT_ID); my $sJsonText = ACTINIC::EncodeJson(\%sParams); $SSLConnection->SendRequest($sJsonText); # # 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() == 422) { # # For the Merchant Key lookup the error is not meaningful to the buyer # In this case we show the error only in the error file # ACTINIC::RecordErrors("Sage Pay integration error:\r\n" . GetServerError($SSLConnection->GetResponseJSON()), ACTINIC::GetPath()); return ($::FAILURE, "Payment service is temporarily unavailable.\r\nPlease try a different payment method.", ""); } if ($SSLConnection->GetConnectStatus() == $::FALSE) { ACTINIC::RecordErrors(sprintf("%s (%s) %s", "Sage Pay connection failed:", $SSLConnection->GetResponseCode(), $SSLConnection->GetConnectErrorMessage()), ACTINIC::GetPath()); return ($::FAILURE, "Payment service is temporarily unavailable.\r\nPlease try a different payment method.", ""); } # # Now fetch the content as key value pairs # my $sMerchantkey = ""; if (ref($SSLConnection->GetResponseJSON()) eq 'HASH') { $sMerchantkey = $SSLConnection->GetResponseJSON()->{'merchantSessionKey'}; } if ($sMerchantkey eq "") { ACTINIC::RecordErrors("Sage Pay Integration: merchantSessionKey not returned", ACTINIC::GetPath()); return ($::FAILURE, "Payment service is temporarily unavailable.\r\nPlease try a different payment method.", ""); } return ($::SUCCESS, "", $sMerchantkey); } ###################################################################### # # RegisterServerTransaction - Register the payment details with Sage Pay # # Returns 0 - status $::SUCCESS or $::FAILURE # 1 - error message if not $::SUCCESS # 2 - form html # ###################################################################### sub RegisterServerTransaction { # # Construct the details to be passed to the PSP # my %hashParams; $hashParams{'VPSProtocol'} = "3.00"; if ($bAuthenticate) # only authenticate { $hashParams{'TxType'} = "AUTHENTICATE"; } elsif (!$bAuthorize) # pre-authorize mode { $hashParams{'TxType'} = "DEFERRED"; } else # immediate payment { $hashParams{'TxType'} = "PAYMENT"; } $hashParams{'Vendor'} = $::PSP_MERCHANT_ID; # # VendorTxCode needs a random element to ensure this code has not been used before # $hashParams{'VendorTxCode'} = $::sOrderNumber . "-" . int(rand(100000)); # # VPS requires decimal places in the amount (not lowest digits, so work them out). # my $nNumDigits = $::PriceFormatBlob{"ICURRDIGITS"}; # read the currency format values my ($nAmount, $nFactor, $sAmount); if(defined $nNumDigits) {$nFactor = (10 ** $nNumDigits);} else {$nFactor = 100;} $sAmount = sprintf("%d.%02d", $::nOrderTotal / $nFactor, $::nOrderTotal % $nFactor); $hashParams{'Amount'} = $sAmount; $hashParams{'Currency'} = $::PriceFormatBlob{SINTLSYMBOLS}; $hashParams{'Description'} = ACTINIC::EncodeText2("Items from " . $::PSP_MERCHANT_ID, $::FALSE); # # Construct the authorisation call back # my $sURL = sprintf("%sACTION=INCONTEXTPSP&ON=%s&CARTID=%s&JS=1", GetCheckoutBaseUrl(), $::sOrderNumber, $::sCartID); $hashParams{'NotificationURL'} = ACTINIC::EncodeText2($sURL, $::FALSE); # # add the invoice address and customer name # my ($sFirstName, $sLastName); $sLastName = $::InvoiceContact{NAME}; # default to a blank first name and complete last name if ($sLastName =~ /^(.+)\s+(\S+)/) # if the name field looks to contain at least two name parts { $sFirstName = $1; # break the name up $sLastName = $2; } my ($sCountry, $sState) = GetProtxLocationCodes('INVOICE'); $hashParams{'BillingSurname'} = $sLastName; $hashParams{'BillingFirstnames'} = $sFirstName; $hashParams{'BillingAddress1'} = $::InvoiceContact{ADDRESS1}; $hashParams{'BillingAddress2'} = $::InvoiceContact{ADDRESS2}; $hashParams{'BillingCity'} = $::InvoiceContact{ADDRESS3}; $hashParams{'BillingState'} = $sState; $hashParams{'BillingCountry'} = $sCountry; $hashParams{'BillingPostCode'} = substr($::InvoiceContact{POSTALCODE}, 0, 10); if (length($::InvoiceContact{PHONE})!=0) { $hashParams{'BillingPhone'} = $::InvoiceContact{PHONE}; } # # Add the delivery address # $sLastName = $::OCCShipData{NAME}; # default to a blank first name and complete last name if ($sLastName =~ /^(.+)\s+(\S+)/) # if the name field looks to contain at least two name parts { $sFirstName = $1; # break the name up $sLastName = $2; } ($sCountry, $sState) = GetProtxLocationCodes('DELIVERY'); $hashParams{'DeliverySurname'} = $sLastName; $hashParams{'DeliveryFirstnames'} = $sFirstName; $hashParams{'DeliveryAddress1'} = $::OCCShipData{ADDRESS1}; $hashParams{'DeliveryAddress2'} = $::OCCShipData{ADDRESS2}; $hashParams{'DeliveryCity'} = $::OCCShipData{ADDRESS3}; $hashParams{'DeliveryState'} = $sState; $hashParams{'DeliveryCountry'} = $sCountry; $hashParams{'DeliveryPostCode'} = substr($::OCCShipData{POSTALCODE}, 0, 10); if (length($::OCCShipData{PHONE}) != 0) { $hashParams{'DeliveryPhone'} = $::OCCShipData{PHONE}; } # # Add confirmation email addresses if present. # if (length($::InvoiceContact{EMAIL})!=0) { $hashParams{'CustomerEMail'} = $::InvoiceContact{EMAIL}; } if (length($::PSP_CONFIRMATION_EMAIL)!=0) { $hashParams{'VendorEMail'} = $::PSP_CONFIRMATION_EMAIL; } $hashParams{'eMailMessage'} = "You can put your own message in here"; $hashParams{'AllowGiftAid'} = 0; # # ApplyAVSCV2 # 0 = If AVS/CV2 enabled then check them. If rules apply, use rules (default) # 1 = Force AVS/CV2 checks even if not enabled for the account. If rules apply, use rules. # 2 = Force NO AVS/CV2 checks even if enabled on account. # 3 = Force AVS/CV2 checks even if not enabled for the account but DON’T apply any rules. # $hashParams{'ApplyAVSCV2'} = 0; # # Apply3DSecure # 0 = If 3D-Secure checks are possible and rules allow, perform the checks and apply the authorisation rules. (default) # 1 = Force 3D-Secure checks for this transaction if possible and apply rules for authorisation. # 2 = Do not perform 3D-Secure checks for this transaction and always authorise. # 3 = Force 3D-Secure checks for this transaction if possible but ALWAYS obtain an auth code, irrespective of rule base # $hashParams{'Apply3DSecure'} = 0; my $sWebSite = $::PSP_AUTHORISE_URL; $sWebSite =~ s/http(s?):\/\///i; $hashParams{'Website'} = $sWebSite; $hashParams{'ReferrerID'} = $sReferrerID; # # Get a secure connection # $sProcessScriptURL =~ /^https?:\/\/(.*?)(\/.*)$/; my $SSLConnection = SSLConnection->new($1, $::PSP_SECURE_SERVER_PORT, $2); $SSLConnection->SetRequestMethod("POST"); $SSLConnection->SetRequestTimeout(15); # set timeout to 15 seconds # # Send the request to Sage Pay # $SSLConnection->SendRequest(GetParamString(\%hashParams)); # # Check for general connection error # if ($SSLConnection->GetConnectStatus() == $::FALSE) { my $sError = sprintf("%s (%s) %s", "Sage Pay connection failed:", $SSLConnection->GetResponseCode(), $SSLConnection->GetConnectErrorMessage()); ACTINIC::RecordErrors($sError, ACTINIC::GetPath()); $sError = "Error contacting Sage Pay, please try again or select another payment method"; return ($::PENDING, $sError, GetHTMLError()); } # # Now fetch the content as key value pairs # my $hashResult = ParseNqvResponse($SSLConnection->GetResponseContent()); if ($hashResult->{'Status'} ne "OK") { my $sError = sprintf("Sage Pay payment request failed. Order:%s Status:%s %s", $::sOrderNumber, $hashResult->{'Status'}, $hashResult->{'StatusDetail'}); return ($::PENDING, $sError, GetHTMLError()); } # # Record the Sage Pay IDs for later use # $::Session->SetSagePayIDs($hashResult->{'SecurityKey'}, $hashResult->{'VPSTxId'}); # # Now we build the response by returning an empty form with the # bounce URL received from Sage Pay as the form Action # my (%VarTable); $VarTable{$::VARPREFIX . 'OCC_URL'} = $hashResult->{'NextURL'}; $VarTable{$::VARPREFIX . 'OCC_VALUES'} = ''; # no OCC values for the template my $sLinkHTML = 'occlink.html'; if(defined $::g_pPaymentList) { $sLinkHTML = $$::g_pPaymentList{ActinicOrder::PaymentStringToEnum($::g_PaymentInfo{'METHOD'})}{BOUNCE_HTML}; } @Response = ACTINIC::TemplateFile($::sPath . $sLinkHTML, \%VarTable); # build the file if ($Response[0] != $::SUCCESS) { my $sError = sprintf("Sage Pay payment request failed while parsing template. Order:%s Status:%s", $::sOrderNumber, $Response[1]); ACTINIC::RecordErrors($sError, ACTINIC::GetPath()); $sError = "Error contacting Sage Pay, please try again or select another payment method"; return ($::FAILURE, $sError, GetHTMLError()); } @Response = ACTINIC::MakeLinksAbsolute($Response[2], $::sWebSiteUrl, $::sContentUrl); if ($Response[0] != $::SUCCESS) { my $sError = sprintf("Sage Pay payment request failed while making links absolute. Order:%s Status:%s", $::sOrderNumber, $Response[1]); ACTINIC::RecordErrors($sError, ACTINIC::GetPath()); $sError = "Error contacting Sage Pay, please try again or select another payment method"; return ($::FAILURE, $sError, GetHTMLError()); } my $sHTML = $Response[2]; # grab the resulting HTML # # process the test mode warning # my ($sDelimiter) = $::DELPREFIX . 'TESTMODE'; if ($bTestMode) # only include the test mode block if we are in test mode { $sHTML =~ s/$sDelimiter//g; # remove the delimiter text } else # not in test mode - remove the block { $sHTML =~ s/$sDelimiter(.*?)$sDelimiter//gs; # remove the test mode warning blob (/s removes the \n limitation of .) } return ($::SUCCESS, "", $sHTML); } ###################################################################### # # RecordServerPayment - Record the payment # *** IMPORTANT *** # This method must return a plain text NQV response to Sage Pay # and a redirect URL for either success (receipt) or failure (back) # This method must not return to the calling mehod # ###################################################################### sub RecordServerPayment { my ($sResponseStatus, $sURL, $sMessage, $sMsgFormat); # # Set up the default response fields # $sResponseStatus = "ERROR"; $sMessage = "An unexpected error occurred"; $sMsgFormat = "&ERROR=There was a problem while recording your order. No payment has been taken." . " You can try again with the same or a different payment method or you can contact us with details of this problem." . " The error was: %s"; $sURL = sprintf("%sACTION=%s&SEQUENCE=3&ON=%s&CARTID=%s&JS=1", GetCheckoutBaseUrl(), ACTINIC::GetPhrase(-1, 503), $::sOrderNumber, $::sCartID); # # Read the POSTed data # my ($status, $sError, $hashResult, $sPostedData) = ReadPostData(); if ($status != $::SUCCESS) { ACTINIC::RecordErrors("Error recording Sage Pay payment: $sError", ACTINIC::GetPath()); $sMessage = "Unable to read the POST data"; } else { ($sResponseStatus, $sMessage) = CheckCallBack($hashResult); if ($hashResult->{'Status'} eq 'ABORT') { # # ABORTs can be received for incomplete transactions where the buyer may # have started a new transaction in which case the transaction details # won't match so we cannot verify that this is a valid request from Sage Pay. # We shall simply acknowledge the aborted transaction # $sResponseStatus = "OK"; # always OK abort messages $sMessage = ""; # no need to tell the user they will already know } # # For all other call backs the signature must be valid # elsif ($sResponseStatus eq "OK") { # # If status is OK, AUTHENTICATED or REGISTERED then record the payment # if (($hashResult->{'Status'} eq 'OK') || ($hashResult->{'Status'} eq 'AUTHENTICATED') || ($hashResult->{'Status'} eq 'REGISTERED')) { # # If authorize and not authenticate then not pre-authorize mode # my $bPreAuthorise = ($bAuthorize && !$bAuthenticate) ? $::FALSE : $::TRUE; # # Errors recording the authorisation will be logged but they # will not stop the process as the payment has been taken # RecordPayment($hashResult, $bPreAuthorise); SendDelayedEmails($::sOrderNumber); # send delayed emails if any $sURL = $::sCallBackURLUser . "CARTID=$::sCartID"; #the URL of the receipt script $sMessage = ""; # # We only save the session file here as it is the only path where the session is changed # $::Session->SaveSession(); # save our session file } elsif ($hashResult->{'Status'} eq 'ERROR') { ACTINIC::RecordErrors(NotifyOfError("Sage Pay has returned an ERROR status for VSPTxId " . $hashResult->{'VPSTxId'} . "\r\nSage Pay error: " . $hashResult->{'StatusDetail'}, $::TRUE), ACTINIC::GetPath()); $sResponseStatus = "OK"; # we acknowledge receipt of the ERROR response $sMessage = ""; } else { # # The remaining status values are all failed transactions # Merchant can view these at MySagePay # $sMessage = $hashResult->{'StatusDetail'}; } } } if ($sMessage ne "") { $sURL .= sprintf($sMsgFormat, $sMessage); } my $sResponse = sprintf("Status=%s\r\nRedirectURL=%s\r\nStatusDetail=%s\r\n", $sResponseStatus, $sURL, $sMessage); LogData("Authorisation response sent to Sage Pay\r\n$sResponse"); ACTINIC::PrintText($sResponse); # reply to the PSP exit; # we have responded to the PSP so end here } ###################################################################### # # CheckCallBack - Check the callback is valid # # Input 0 - hash of received fields # # Returns 0 - status OK, INVALID or ERROR # 1 - error message if not $::SUCCESS # ###################################################################### sub CheckCallBack { my ($hashResult) = shift; # # We need to check the security details before accepting # my ($sKey, $sTxId) = $::Session->GetSagePayIDs(); if ($hashResult->{'VPSTxId'} ne $sTxId) { my $sError = sprintf("Sage Pay Expected TxnID=%s Received TxnID=%s", $sTxId, $hashResult->{'VPSTxId'}); if ($hashResult->{'Status'} eq 'ABORT') { LogData($sError); # only log the ABORT in debug } else { ACTINIC::RecordErrors($sError, ACTINIC::GetPath()); } return ("INVALID", "Call Back Transaction ID does not match the expected ID"); } my ($status, $sError, $sSignature); $sSignature .= $hashResult->{'VPSTxId'}; $sSignature .= $hashResult->{'VendorTxCode'}; $sSignature .= $hashResult->{'Status'}; $sSignature .= $hashResult->{'TxAuthNo'}; $sSignature .= lc($::PSP_MERCHANT_ID); $sSignature .= $hashResult->{'AVSCV2'}; $sSignature .= $sKey; $sSignature .= $hashResult->{'AddressResult'}; $sSignature .= $hashResult->{'PostCodeResult'}; $sSignature .= $hashResult->{'CV2Result'}; $sSignature .= $hashResult->{'GiftAid'}; $sSignature .= $hashResult->{'3DSecureStatus'}; $sSignature .= $hashResult->{'CAVV'}; $sSignature .= $hashResult->{'AddressStatus'}; $sSignature .= $hashResult->{'PayerStatus'}; $sSignature .= $hashResult->{'CardType'}; $sSignature .= $hashResult->{'Last4Digits'}; $sSignature .= $hashResult->{'DeclineCode'}; $sSignature .= $hashResult->{'ExpiryDate'}; $sSignature .= $hashResult->{'FraudResponse'}; $sSignature .= $hashResult->{'BankAuthCode'}; if ($hashResult->{'VPSSignature'} ne uc(ACTINIC::GetMD5Hash($sSignature))) { my $sError = sprintf("TxnID=%s SIG=%s MD5=%s", $sTxId, $hashResult->{'VPSSignature'}, uc(ACTINIC::GetMD5Hash($sSignature))); if ($hashResult->{'Status'} eq 'ABORT') { LogData($sError); # only log the ABORT in debug } else { ACTINIC::RecordErrors($sError, ACTINIC::GetPath()); } return ("INVALID", "Signatures do not match"); } return ("OK", ""); } ###################################################################### # # GetPSPPayment - Get the payment details from Sage Pay # # Returns 0 - status $::SUCCESS or $::FAILURE # 1 - error message if not $::SUCCESS # 2 - must be empty string or 3DSecure bounce page # ###################################################################### sub GetPSPPayment { # # Check we have a token # if ((!defined $::g_InputHash{'TOKEN'}) || ($::g_InputHash{'TOKEN'} eq "")) { return ($::PENDING, "Sage Pay Integration: Payment token is missing", GetHTMLError()); } # # Construct the details to be passed to the PSP # my %hashParams; my $bPreAuthorise = $::TRUE; $hashParams{'VPSProtocol'} = "3.00"; $hashParams{'Token'} = $::g_InputHash{'TOKEN'}; $hashParams{'ECDType'} = "1"; if ($bAuthenticate) # only authenticate { $hashParams{'TxType'} = "AUTHENTICATE"; } elsif (!$bAuthorize) # pre-authorize mode { $hashParams{'TxType'} = "DEFERRED"; } else # immediate payment { $hashParams{'TxType'} = "PAYMENT"; $bPreAuthorise = $::FALSE; } $hashParams{'Vendor'} = $::PSP_MERCHANT_ID; # # VendorTxCode needs a random element to ensure this code has not been used before # $hashParams{'VendorTxCode'} = $::sOrderNumber . "-" . int(rand(100000)); # # VPS requires decimal places in the amount (not lowest digits, so work them out). # my $nNumDigits = $::PriceFormatBlob{"ICURRDIGITS"}; # read the currency format values my ($nAmount, $nFactor, $sAmount); if(defined $nNumDigits) {$nFactor = (10 ** $nNumDigits);} else {$nFactor = 100;} $sAmount = sprintf("%d.%02d", $::nOrderTotal / $nFactor, $::nOrderTotal % $nFactor); $hashParams{'Amount'} = $sAmount; $hashParams{'Currency'} = $::PriceFormatBlob{SINTLSYMBOLS}; $hashParams{'Description'} = "Items from " . $::PSP_MERCHANT_ID; # # add the invoice address and customer name # my ($sFirstName, $sLastName); $sLastName = $::InvoiceContact{NAME}; # default to a blank first name and complete last name if ($sLastName =~ /^(.+)\s+(\S+)/) # if the name field looks to contain at least two name parts { $sFirstName = $1; # break the name up $sLastName = $2; } my ($sCountry, $sState) = GetProtxLocationCodes('INVOICE'); $hashParams{'BillingSurname'} = $sLastName; $hashParams{'BillingFirstnames'} = $sFirstName; $hashParams{'BillingAddress1'} = $::InvoiceContact{ADDRESS1}; $hashParams{'BillingAddress2'} = $::InvoiceContact{ADDRESS2}; $hashParams{'BillingCity'} = $::InvoiceContact{ADDRESS3}; $hashParams{'BillingState'} = $sState; $hashParams{'BillingCountry'} = $sCountry; $hashParams{'BillingPostCode'} = substr($::InvoiceContact{POSTALCODE}, 0, 10); if (length($::InvoiceContact{PHONE})!=0) { $hashParams{'BillingPhone'} = $::InvoiceContact{PHONE}; } # # Add the delivery address # $sLastName = $::OCCShipData{NAME}; # default to a blank first name and complete last name if ($sLastName =~ /^(.+)\s+(\S+)/) # if the name field looks to contain at least two name parts { $sFirstName = $1; # break the name up $sLastName = $2; } ($sCountry, $sState) = GetProtxLocationCodes('DELIVERY'); $hashParams{'DeliverySurname'} = $sLastName; $hashParams{'DeliveryFirstnames'} = $sFirstName; $hashParams{'DeliveryAddress1'} = $::OCCShipData{ADDRESS1}; $hashParams{'DeliveryAddress2'} = $::OCCShipData{ADDRESS2}; $hashParams{'DeliveryCity'} = $::OCCShipData{ADDRESS3}; $hashParams{'DeliveryState'} = $sState; $hashParams{'DeliveryCountry'} = $sCountry; $hashParams{'DeliveryPostCode'} = substr($::OCCShipData{POSTALCODE}, 0, 10); if (length($::OCCShipData{PHONE}) != 0) { $hashParams{'DeliveryPhone'} = $::OCCShipData{PHONE}; } # # Add confirmation email addresses if present. # if (length($::InvoiceContact{EMAIL})!=0) { $hashParams{'CustomerEMail'} = $::InvoiceContact{EMAIL}; } if (length($::PSP_CONFIRMATION_EMAIL)!=0) { $hashParams{'VendorEMail'} = $::PSP_CONFIRMATION_EMAIL; } $hashParams{'eMailMessage'} = "You can put your own message in here"; $hashParams{'AllowGiftAid'} = 0; # # ApplyAVSCV2 # 0 = If AVS/CV2 enabled then check them. If rules apply, use rules (default) # 1 = Force AVS/CV2 checks even if not enabled for the account. If rules apply, use rules. # 2 = Force NO AVS/CV2 checks even if enabled on account. # 3 = Force AVS/CV2 checks even if not enabled for the account but DON’T apply any rules. # $hashParams{'ApplyAVSCV2'} = 0; # # Apply3DSecure # 0 = If 3D-Secure checks are possible and rules allow, perform the checks and apply the authorisation rules. (default) # 1 = Force 3D-Secure checks for this transaction if possible and apply rules for authorisation. # 2 = Do not perform 3D-Secure checks for this transaction and always authorise. # 3 = Force 3D-Secure checks for this transaction if possible but ALWAYS obtain an auth code, irrespective of rule base # $hashParams{'Apply3DSecure'} = 0; my $sWebSite = $::PSP_AUTHORISE_URL; $sWebSite =~ s/http(s?):\/\///i; $hashParams{'Website'} = $sWebSite; $hashParams{'ReferrerID'} = $sReferrerID; # # Get a secure connection # my $SSLConnection = SSLConnection->new($::PSP_HOST, $::PSP_SECURE_SERVER_PORT, $::PSP_DIRECT_PATH); $SSLConnection->SetRequestMethod("POST"); $SSLConnection->SetRequestTimeout(15); # set timeout to 15 seconds # # Send the request to Sage Pay # $SSLConnection->SendRequest(GetParamString(\%hashParams)); # # Check for general connection error # if ($SSLConnection->GetConnectStatus() == $::FALSE) { my $sError = sprintf("%s (%s) %s", "Sage Pay connection failed:", $SSLConnection->GetResponseCode(), $SSLConnection->GetConnectErrorMessage()); return ($::PENDING, $sError, GetHTMLError()); } # # Now fetch the content as key value pairs # my $hashResult = ParseNqvResponse($SSLConnection->GetResponseContent()); # # Check the status to determine if the payment/authorisation has been accepted # if ($hashResult->{'Status'} eq "3DAUTH") { # # We need to redirect the buyer for 3DSecure check # return (Get3DSecureHtml($hashResult->{'ACSURL'}, $hashResult->{'PAReq'}, $hashResult->{'MD'})); } if (($hashResult->{'Status'} ne "OK") && ($hashResult->{'Status'} ne "AUTHENTICATED")) { my $sError = sprintf("Sage Pay payment request failed. Order:%s Status:%s %s", $::sOrderNumber, $hashResult->{'Status'}, $hashResult->{'StatusDetail'}); return ($::PENDING, $sError, GetHTMLError()); } RecordPayment($hashResult, $bPreAuthorise); return ($::SUCCESS, "", ""); # all done } ###################################################################### # # Get3DSecurePayment - Get the payment details from Sage Pay # after 3D Secure callback # # Returns 0 - status $::SUCCESS or $::FAILURE # 1 - error message if not $::SUCCESS # 2 - must be empty string # ###################################################################### sub Get3DSecurePayment { my %hashParams; my ($status, $sError, $pmapInputNameToValue, $sPostedData) = ReadPostData(); if ($status != $::SUCCESS) { return ($::PENDING, $sError, GetHTMLError()); } $hashParams{'MD'} = $pmapInputNameToValue->{'MD'}; $hashParams{'PARes'} = $pmapInputNameToValue->{'PaRes'}; # # Get a secure connection # my $SSLConnection = SSLConnection->new($::PSP_HOST, $::PSP_SECURE_SERVER_PORT, $::PSP_3DSECURE_PATH); $SSLConnection->SetRequestMethod("POST"); $SSLConnection->SetRequestTimeout(15); # set timeout to 15 seconds # # Send the request to Sage Pay # $SSLConnection->SendRequest(GetParamString(\%hashParams)); # # Check for general connection error # if ($SSLConnection->GetConnectStatus() == $::FALSE) { my $sError = sprintf("%s (%s) %s", "Sage Pay connection failed:", $SSLConnection->GetResponseCode(), $SSLConnection->GetConnectErrorMessage()); return ($::PENDING, $sError, GetHTMLError()); } # # Now fetch the content as key value pairs # my $hashResult = ParseNqvResponse($SSLConnection->GetResponseContent()); if (($hashResult->{'Status'} ne "OK") && ($hashResult->{'Status'} ne "AUTHENTICATED")) { my $sError = sprintf("Sage Pay payment request failed. Order:%s Status:%s %s", $::sOrderNumber, $hashResult->{'Status'}, $hashResult->{'StatusDetail'}); return ($::PENDING, $sError, GetHTMLError()); } # # If authorize and not authenticate then not pre-authorize mode # my $bPreAuthorise = ($bAuthorize && !$bAuthenticate) ? $::FALSE : $::TRUE; RecordPayment($hashResult, $bPreAuthorise); return ($::SUCCESS, "", ""); # all done } ###################################################################### # # RecordPayment - Record the payment # # Params 0 - hashed result of request # 1 - $::TRUE if Pre-Authorise # ###################################################################### sub RecordPayment { my ($hashResult, $bPreAuthorise) = @_; # # Set the input parameters as expected by RecordAuthorization # my ($status, $sMessage, $sOrderNumber) = GetOrderNumber(); 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} = $bTestMode ? 1 : 0; # # Sage Pay do not return the authorised amount so assume the order total # $::g_InputHash{AM} = $::nOrderTotal; $::g_InputHash{ACTION} = sprintf("AUTHORIZE_%d", $::g_PaymentInfo{'METHOD'}); # # Build the parameter list for the OCC file # my ($sDate) = ACTINIC::GetActinicDateWithSeconds(); ($sDate) = ACTINIC::EncodeText2($sDate, $::FALSE); # # Remove the surrounding {} brackets to match how it appears in Sage Pay MIS # my $sCDValue = $hashResult->{'VPSTxId'}; $sCDValue =~ s/{(.*)}/$1/; my $sParams = sprintf("ON=%s&TM=%s&PA=%s&AM=%s&TX=%s&CD=%s&DT=%s", $::g_InputHash{ON}, $::g_InputHash{TM}, ($bPreAuthorise) ? "1" : "0", $::g_InputHash{AM}, $sCDValue, $hashResult->{'TxAuthNo'}, $sDate); # # PSP Response text # $sParams .= '&PR=' . $hashResult->{'StatusDetail'}; # # Add verification results # $sParams .= '&RA=' . $hashResult->{'AddressResult'}; $sParams .= '&RC=' . $hashResult->{'CV2Result'}; $sParams .= '&ZR=' . $hashResult->{'PostCodeResult'}; # # Add 3D Secure # $sParams .= '&3E=' . $hashResult->{'Status'}; $sParams .= '&3R=' . $hashResult->{'3DSecureStatus'}; # # Create the signature # my $sToSign = $sParams . "&&" . $::PSP_KEY; my $sSignature = ACTINIC::GetMD5Hash($sToSign); $sParams .= sprintf("&SN=%s", $sSignature); # # Record the authorization # my $sError = RecordAuthorization(\$sParams); $::g_InputHash{ACTION} = $sAction; # restore the original action if (length $sError != 0) { # # Record any RecordAuthorization error # We continue as the order is paid for # ACTINIC::RecordErrors("RecordAuthorization failed - $sError", ACTINIC::GetPath()); } } ###################################################################### # # ParseNqvResponse - Parse the authorisation response # # Input 0 - multi line list of key=value pairs # # Returns 0 - hash of results # ###################################################################### sub ParseNqvResponse { my $sResponse = shift; my %hashResult; my @aLines = split(/\r\n/, $sResponse); # Sage Pay are using CRLF line breaks foreach my $sResult (@aLines) { $sResult =~ /^(.*?)=(.*)/; if ($1 ne "") { $hashResult{$1}=$2; } } return (\%hashResult); } ###################################################################### # # GetServerError - Get the server error message # # Input 0 - hash of error # # Returns 0 - error message # ###################################################################### sub GetServerError { my ($hashErrors) = shift; # # Expecting a hash key 'errors' which should contain an array of errors # Each error consists of a hash of keys # description # property # code # my $sErrorMessage; if ((ref($hashErrors) eq 'HASH') && (defined $hashErrors->{'errors'}) && (ref($hashErrors->{'errors'}) eq 'ARRAY')) { my ($aErrors) = $hashErrors->{'errors'}; foreach my $hashError (@{$aErrors}) { my $sError = sprintf("(%s) %s Field %s", $hashError->{'code'}, $hashError->{'description'}, $hashError->{'property'}); $sErrorMessage .= $sError . "\r\n"; } } return ($sErrorMessage); } ###################################################################### # # GetHTMLError - Get the HTML form of the error message # # Returns 0 - error message # ###################################################################### sub GetHTMLError { my $sHTML = <<"END_HTML"; Your order $::sOrderNumber has been received but there was a problem collecting the payment. Perhaps the invoice address below does not match the address on the card statement.
No payment has been charged to your card.
You may:-
- try again to complete the payment
- choose another payment method
- contact the merchant regarding payment
END_HTML return ($sHTML); } ################################################################ # # GetProtxLocationCodes - Get the appropriate country and region codes # # The merchant's country code is used if a country hasn't been specified # This is to handle the case where the merchant only ships to their own # country and they use simple tax and shipping. # # UK is translated to the ISO code GB # # If UK has states specified, the region code is blanked. # # Input: $sAddress - 'INVOICE' or 'DELIVERY' # # Returns: ($sCountryCode, $sStateCode) # ################################################################ sub GetProtxLocationCodes { my ($sAddress) = @_; # # Handle country code first # my $sCountryCode = $::g_LocationInfo{$sAddress . '_COUNTRY_CODE'}; if ($sCountryCode eq '') { $sCountryCode = $::g_pSetupBlob->{'MERCHANT_COUNTRY_CODE'}; } # # Handle state code according to country # my $sStateCode = ''; if ($sCountryCode eq 'UK') { $sCountryCode =~ s/^UK$/GB/; } elsif ($sCountryCode eq 'US') { my $sRegionKey = $sAddress . '_REGION_CODE'; $sStateCode = $::g_LocationInfo{$sRegionKey}; $sStateCode = ($sStateCode ne $ActinicOrder::UNDEFINED_REGION) ? ActinicLocations::GetISORegionCode($sStateCode) : ""; } return ($sCountryCode, $sStateCode); } # # Base64 encoding # 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; } # # Base64 decoding # sub Base64Decode ($) { local($^W) = 0; my $str = shift; $str =~ tr|A-Za-z0-9+=/||cd; # remove non-base64 chars if (length($str) % 4) { require Carp; Carp::carp("Length of base64 data not a multiple of 4") } $str =~ s/=+$//; # remove padding $str =~ tr|A-Za-z0-9+/| -_|; # convert to uuencoded format return join'', map( unpack("u", chr(32 + length($_)*3/4) . $_), $str =~ /(.{1,60})/gs); } ############################################################### # # GetParamString - get the parameter list as a name/value # pair list as required by Sage Pay # # Return: [0] - the concatenated string parameter list # ############################################################### sub GetParamString { my ($hashParams) = @_; my $sParamString; # # Concatenate the params # foreach my $sParam (keys %{$hashParams}) { $sParamString .= sprintf("%s=%s", $sParam, $$hashParams{$sParam}) ."&"; } # # Trim last & # $sParamString =~ s/\&$//; return($sParamString); } ############################################################### # # Get3DSecureHtml - get the html for the 3D Secure page # # Params [0] - 3DSecure URL (ACSURL) # [1] - encrypted paramater (PAReq) # [2] - transaction identifier (MD) # # Return: [0] - status # [1] - error message if not success # [2] - the page HTML # ############################################################### sub Get3DSecureHtml { my ($sACSURL, $sPAReq, $sMD) = @_; my ($sHTML, $sHiddenValues, %VarTable, $Response, $sTermUrl); $VarTable{$::VARPREFIX . 'OCC_URL'} = $sACSURL; # insert the 3DSecure URL into the HTML template # # We include the cart ID as on return we won't have the buyer's cookie # We also add JS=1 which means JS is enabled which is must # have been to have reached this stage of the process # $sTermUrl = sprintf("%sACTION=INCONTEXTPSP&3DAUTH=1&CARTID=%s&JS=1", GetCheckoutBaseUrl(), $::sCartID); $sHiddenValues .= ""; $sHiddenValues .= ""; $sHiddenValues .= ""; $VarTable{$::VARPREFIX . 'OCC_VALUES'} = $sHiddenValues; # add the OCC values to the template $VarTable{$::VARPREFIX . 'BOUNCEMESSAGE'} = 'You will now be automatically transferred to the 3D Secure server to verify your card details.'; my $sLinkHTML = 'occlink.html'; if(defined $::g_pPaymentList) { $sLinkHTML = $$::g_pPaymentList{ActinicOrder::PaymentStringToEnum($::g_PaymentInfo{'METHOD'})}{BOUNCE_HTML}; } @Response = ACTINIC::TemplateFile($::sPath . $sLinkHTML, \%VarTable); # build the file if ($Response[0] != $::SUCCESS) { return ($::FAILURE, $Response[1], GetHTMLError()); } @Response = ACTINIC::MakeLinksAbsolute($Response[2], $::sWebSiteUrl, $::sContentUrl); if ($Response[0] != $::SUCCESS) { return ($::FAILURE, $Response[1], GetHTMLError()); } $sHTML = $Response[2]; # grab the resulting HTML # # Process the test mode warning # my ($sDelimiter) = $::DELPREFIX . 'TESTMODE'; if ($bTestMode) # only include the test mode block if we are in test mode { $sHTML =~ s/$sDelimiter//g; # remove the delimiter text } else # not in test mode - remove the block { $sHTML =~ s/$sDelimiter(.*?)$sDelimiter//gs; # remove the test mode warning blob (/s removes the \n limitation of .) } return ($::SUCCESS, "", $sHTML); } ####################################################### # # 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 PayPal parameters # 3 - the raw posted data string # ####################################################### sub ReadPostData { my ($InputData, $nInputLength, $nStep, $InputBuffer); $nInputLength = 0; $nStep = 0; while ($nInputLength != $::ENV{'CONTENT_LENGTH'}) # read until you have the entire chunk of data { # # read the input # binmode STDIN; $nStep = read(STDIN, $InputBuffer, $ENV{'CONTENT_LENGTH'}); # read POSTed data $nInputLength += $nStep; # keep track of the total data length $InputData .= $InputBuffer; # append the latest chunk to the total data buffer if (0 == $nStep) # EOF { last; # stop read } } if ($nInputLength != $ENV{'CONTENT_LENGTH'}) { return ($::FAILURE, "Bad input. The data length actually read ($nInputLength) does not match the length specified " . $ENV{'CONTENT_LENGTH'} . "\n", undef, undef); } $InputData =~ s/&$//; # loose any bogus trailing &'s # # Parse the input # my (@listNameValues); @listNameValues = split (/[&=]/, $InputData); # break the input into key/values if ($#listNameValues % 2 != 1) # if there is an unmatched value, it is a trailing = which means the value is undef { push @listNameValues, undef; } # # Decode the input # my %EncodedInput = @listNameValues; # map the input key/values to a hash = note that this doesn't work for things like mult-select lists but we don't have to worry about that here my ($key, $value); my %mapNameToValue; while (($key, $value) = each %EncodedInput) { $mapNameToValue{DecodeText($key)} = DecodeText($value); } return ($::SUCCESS, undef, \%mapNameToValue, $InputData); } ####################################################### # # DecodeText - decode the CGI FORM encoding # # Inputs: 0 - string to decode # # Returns: decoded string # ####################################################### sub DecodeText { my ($sString) = @_; $sString =~ s/\+/ /g; # replace + signs with the spaces they represent $sString =~ s/%([A-Fa-f0-9]{2})/pack('c',hex($1))/ge; # Convert %XX from hex numbers to character equivalent return $sString; } # # Must be at the end as any following code will not be parsed # return ($::SUCCESS);