[interchange-cvs] [SCM] Interchange branch, master, updated. 71584254cd8d3f5dca445aafe6ebe2cbdfd02cae

Jon Jensen interchange-cvs at icdevgroup.org
Tue Sep 1 22:13:01 UTC 2009


This is an automated email from the git hooks/post-receive script. It was
generated because a ref change was pushed to the repository containing
the project "Interchange".

The branch, master has been updated
       via  71584254cd8d3f5dca445aafe6ebe2cbdfd02cae (commit)
       via  c1befd5c891205d6268086887f1bd9ccfb58bbaa (commit)
       via  9ae3f207e9214cfd2b9af13226a0091892f30b29 (commit)
      from  6225168cd500675c7fffa4cb77608d8b81de87cf (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.

- Log -----------------------------------------------------------------
commit 71584254cd8d3f5dca445aafe6ebe2cbdfd02cae
Author: Jon Jensen <jon at endpoint.com>
Date:   Tue Sep 1 16:12:41 2009 -0600

    Add GoogleCheckout and SagePay payment modules by Lyn St George

commit c1befd5c891205d6268086887f1bd9ccfb58bbaa
Author: Jon Jensen <jon at endpoint.com>
Date:   Tue Sep 1 16:11:34 2009 -0600

    Update PaypalExpress module from version 1.0.2 to 1.0.5
    
    Thanks to Lyn St George.

commit 9ae3f207e9214cfd2b9af13226a0091892f30b29
Author: Jon Jensen <jon at endpoint.com>
Date:   Tue Sep 1 16:08:51 2009 -0600

    Clear bogus execute bit on Vend::Payment::Getitcard

-----------------------------------------------------------------------

Summary of changes and diff:
 lib/Vend/Payment/GoogleCheckout.pm | 1286 ++++++++++++++++++++++++++++++++++++
 lib/Vend/Payment/PaypalExpress.pm  |  403 ++++++------
 lib/Vend/Payment/SagePay.pm        | 1245 ++++++++++++++++++++++++++++++++++
 3 files changed, 2737 insertions(+), 197 deletions(-)
 mode change 100755 => 100644 lib/Vend/Payment/Getitcard.pm
 create mode 100644 lib/Vend/Payment/GoogleCheckout.pm
 create mode 100644 lib/Vend/Payment/SagePay.pm

diff --git a/lib/Vend/Payment/Getitcard.pm b/lib/Vend/Payment/Getitcard.pm
old mode 100755
new mode 100644
diff --git a/lib/Vend/Payment/GoogleCheckout.pm b/lib/Vend/Payment/GoogleCheckout.pm
new file mode 100644
index 0000000..2609b52
--- /dev/null
+++ b/lib/Vend/Payment/GoogleCheckout.pm
@@ -0,0 +1,1286 @@
+# Vend::Payment::GoogleCheckout - Interchange Google Checkout support
+#
+# GoogleCheckout.pm, v 0.7.2, May 2009
+#
+# Copyright (C) 2009 Zolotek Resources Ltd. All rights reserved.
+#
+# Author: Lyn St George <info at zolotek.net, http://www.zolotek.net>
+# Based on original code by Mike Heins <mheins at perusion.com> and others.
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public Licence as published by
+# the Free Software Foundation; either version 2 of the Licence, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public Licence for more details.
+#
+# You should have received a copy of the GNU General Public
+# Licence along with this program; if not, write to the Free
+# Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
+# MA  02111-1307  USA.
+
+package Vend::Payment::GoogleCheckout;
+
+=head1 Interchange GoogleCheckout support
+
+ http://kiwi.zolotek.net is the home page with the latest version. Also to be found on
+ Kevin Walsh's excellent Interchange site,  http://interchange.rtfm.info.
+
+=head1 AUTHORS
+ Lyn St George <info at zolotek.net>
+
+=head1 CREDITS
+ Steve Graham, mrlock.com, debugging and documentation
+ Andy Smith, tvcables.co.uk, debugging and documentation
+
+
+=head1 PREREQUISITES
+XML::Simple and any of its prerequisites (eg XML::Parser or XML::SAX)
+MIME::Base64
+these should be found anyway on any well-used perl installation. This version was built especially 
+without the Google libraries, so as to work on old machines with perl 5.6.1 and similarly vintaged
+OSes, and only need XML::Simple. 
+
+Interchange.cfg should contain this line, with args extra to 'rand' for current UTF8 problems:
+SafeUntrap  rand require caller dofile print entereval
+
+=head1 DESCRIPTION
+This integrates Google Checkout quite tightly into Interchange - it is expected that all coupons,
+gift certificates, discounts, tax routines and shipping will be processed by Interchange before
+sending the customer to Google. The customer will see the basket, tax, shipping and total cost
+when he arrives there. The shipping is sent as a final value, while the tax is sent as a rate for
+Google to calculate with. The total cost is calculated by Google, and there is the possibility of
+penny differences due to rounding errors or different rounding methods employed in different countries.
+As Google won't accept final tax or total cost figures this cannot be helped.
+
+This module will authorise and optionally charge the customer's card for the purchase. It will handle
+all IPNs (notifications sent back by Google of various events in the process) and both log the results
+and send emails to the merchant and customer as appropriate. IPNs relating to chargebacks, refunds
+and cancellations are included in this. It can handle commands sent through an admin panel - though
+the current IC panel is not set up to send these. Commands relating to 'charge', 'refund', 'cancel' etc
+would normally be sent through the Google admin panel, but the resulting IPNs are handled by this module.
+
+In the interests of tighter integration and simplicity, the consensus of opinion has been to allow
+shipping to only the address taken by Interchange. This means that the shipping charge and tax rate
+will be correct, but also means that the customer cannot choose another address which he may have on
+file at Google - if he does then the system will tell him that "Merchant xx does not ship to this
+address". He of course has the option of clicking "edit basket" and changing his address at Interchange
+to suit. You may want to display some sort of note to this effect prior to sending the customer to
+Google. If delivery is to a US address then the restriction is to the state and the first 3 digits of
+the zip code; if to anywhere else in the world then the restriction is to that country and the first
+3 characters of the postal code (or fewer characters if fewer were entered - if no postal code was
+entered then the whole country is allowed).
+
+It is likely that I will build another version which does allow the customer to change his address whilst at
+Google, as this limitation is a little too harsh and not at all helpful for good customer relations. 
+
+=head2 NOTE
+##########################################################################################################
+# While you can send &amp; and have it returned safely, you cannot send &lt;, &gt; or similar in the     #
+# description field, as it will cause IC to throw an error when Google returns the XML. Even if you send #
+# these as &#x3c; and similar UTF-8 entities, Google will return them in the &lt; format.                #
+#                                                                                                        #
+# Note also that you can only send the currency that is "associated with your seller account", as        #
+# defined by Google. There is no option to configure this, and they select it according to the country   #
+# you registered in your sign-up account. Nor can you do recurring/subscription billing.                 #                                              #
+##########################################################################################################
+
+=head1 SYNOPSIS
+
+Go to http://checkout.google.com and set up a seller's account. They will give you a merchantid and
+a merchantkey, which you need to enter into the payment route as described below. While there, go to
+the "Settings" tab, then the "Integration" left menu item, and set the radio button for "XML call back",
+and enter the URL to your callback page -  https://your_site/ord/gcoipn.
+
+Place this module in your Vend/Payment/ directory, and call it in interchange.cfg with:
+Require module Vend::Payment::GoogleCheckout
+
+Add these configuration options to the new Google payment route in catalog.cfg:
+Route googlecheckout merchantid  (as given to you by Google at sign-up)
+Route googlecheckout merchantkey (as given to you by Google at sign-up)
+Route googlecheckout googlehost  'https://checkout.google.com/cws/v2/Merchant' # live
+Route googlecheckout gcoipn_url http://your_url/cgi_link/ord/gcoipn (replace 'your_url' and 'cgi_link' with yours)
+Route googlecheckout currency    'GBP'  (or USD, or any other ISO code accepted by Google)
+Route googlecheckout edit_basket_url (eg 'http://your_url/cgi_link/ord/basket')
+Route googlecheckout continue_shopping_url (eg, http://your_url/cgi_link/index)
+Route googlecheckout bypass_authorization  (1 to bypass, empty to use. See below for details)
+Route googlecheckout default_taxrate (a decimal, eg '0.06', in case the calculation fails)
+Route googlecheckout sender_email (email address appearing in the 'from' and 'reply-to' fields)
+Route googlecheckout merchant_email (email address to which the order should be sent)
+Route googlecheckout receipt_from_merchant (1 to send an email receipt to the customer)
+Route googlecheckout email_auth_charge ('charge' to send receipt after card has been charged, 'auth' after card has been authorised)
+Route googlecheckout html_mail ('1' to send HTML instead of plain text mail)
+Route googlecheckout avs_match_accepted ('full' for full match where AVS is available, 'partial' for partial match, 'none' for no match required. Default is 'partial')
+Route googlecheckout cv2_match_accepted ('yes' for match required, 'none' for no match required. Default is 'yes')
+Route googlecheckout default_country (2 letter ISO country code; use if not taking a delivery country at checkout, otherwise omit)
+Route googlecheckout default_state (* if you want to allow all states within a country, or omit)
+Route googlecheckout gco_diagnose (1 if you want to return diagnostics, empty otherwise)
+Variable	MV_HTTP_CHARSET	UTF-8
+The last is essential, otherwise GCO will repeat the 'new_order_notification' message ad infinitum and
+never proceed any further. You will be given no clue as to why this is happening. 
+
+NB:/ Apache is not built by default to make the HTTP_AUTHORIZATION header available to the environment,
+and so you will either need to rebuild it or set 'bypass_authorization' to 1 - this latter will not
+check the returned header to see that it contains your merchantid and merchantkey. Google recommend that
+you make this check, but it's your choice.
+
+Add these order routes to catalog.cfg
+Route googlecheckout <<EOF
+	attach            0
+	empty             1
+	default           1
+	supplant          1
+	no_receipt        1
+	report            etc/log_transaction
+	track             logs/tracking.asc
+	counter_tid       logs/tid.counter
+EOF
+
+Route gco_final master 1
+Route gco_final cascade "copy_user main_entry"
+Route gco_final empty 	1
+Route gco_final supplant 1
+Route gco_final no_receipt 1
+Route gco_final email __ORDERS_TO__
+
+The 'edit basket' URL is available to customers when they are at Google, and lets them change either
+the basket contents or the delivery address.
+
+Create a GoogleCheckout button on your checkout page, including the order profile and route like so:
+  [button
+    mv_click=google
+    text="GoogleCheckout"
+    hidetext=1
+    form=checkout
+   ]
+   mv_order_profile=googlecheckout
+   mv_order_route=googlecheckout
+   mv_todo=submit
+  [/button]
+
+Create a page in pages/ord/ called gcoipn.html, consisting of this:
+[charge route="googlecheckout" gcorequest="callback"]
+This page is the target of all IPN callbacks from Google, and will call the payment module in the
+correct mode.
+
+To have GoogleCheckout co-operate with your normal payment service provider, eg Authorizenet, do the
+following:
+
+Add to etc/profiles.order:
+
+__NAME__                            googlecheckout
+__COMMON_ORDER_PROFILE__
+&fatal = yes
+email=required
+email=email
+&set=mv_payment GCO
+&set=psp GCO
+&set=mv_payment_route googlecheckout
+&set=mv_order_route googlecheckout
+&final = yes
+&setcheck = payment_method googlecheckout
+__END__
+or, if you want to use GCO as a 'Buy now' button without taking any customer details, then omit the
+__COMMON_ORDER_PROFILE__ and the two 'email=...' lines above. Google are in fact quite finicky about
+you not taking your customer's details, so you have the option of complying with Google or complying
+with your own policy.
+
+You must have MV_PAYMENT_MODE set in products/variable.txt to either your standard payment processor
+or to 'googlecheckout'; though you may instead set this in catalog.cfg rather than variable txt as:
+Variable MV_PAYMENT_MODE googlecheckout
+
+Within the 'credit_card' section of etc/profiles.order leave
+"MV_PAYMENT_MODE" 
+as set and add
+&set=psp __MV_PAYMENT_PSP__
+&set=mv_payment_route authorizenet
+(or your preferred gateway) as the last entries in the section.
+
+and then add
+Variable MV_PAYMENT_PSP "foo"
+to catalog.cfg, where "foo" is the name of your gateway or acquirer, formatted as you want it to appear
+on the receipt. Eg, "Bank of America" (rather than boa), "AuthorizeNet" (rather than authorizenet).
+
+
+Run the following at a MySQL prompt to add the requisite fields to your transactions table:
+(with thanks to Steve Graham)
+
+ALTER TABLE `transactions` ADD `gco_order_number` VARCHAR( 64 ) CHARACTER SET utf8 COLLATE utf8_general_ci ,
+ADD `gco_buyers_id` VARCHAR( 64 ) CHARACTER SET utf8 COLLATE utf8_general_ci,
+ADD `gco_fulfillment_state` VARCHAR( 64 ) CHARACTER SET utf8 COLLATE utf8_general_ci,
+ADD `gco_serial_number` VARCHAR( 64 ) CHARACTER SET utf8 COLLATE utf8_general_ci,
+ADD `gco_avs_response` VARCHAR( 64 ) CHARACTER SET utf8 COLLATE utf8_general_ci,
+ADD `gco_cvn_response` VARCHAR( 64 ) CHARACTER SET utf8 COLLATE utf8_general_ci,
+ADD `gco_protection` VARCHAR( 64 ) CHARACTER SET utf8 COLLATE utf8_general_ci,
+ADD `gco_cc_number` VARCHAR( 64 ) CHARACTER SET utf8 COLLATE utf8_general_ci,
+ADD `gco_timestamp` VARCHAR( 64 ) CHARACTER SET utf8 COLLATE utf8_general_ci,
+ADD `gco_reason` TEXT CHARACTER SET utf8 COLLATE utf8_general_ci ,
+ADD `gco_latest_charge_amount` VARCHAR( 64 ) CHARACTER SET utf8 COLLATE utf8_general_ci,
+ADD `gco_total_charge_amount` VARCHAR( 64 ) CHARACTER SET utf8 COLLATE utf8_general_ci,
+ADD `gco_latest_chargeback_amount` VARCHAR( 64 ) CHARACTER SET utf8 COLLATE utf8_general_ci,
+ADD `gco_total_chargeback_amount` VARCHAR( 64 ) CHARACTER SET utf8 COLLATE utf8_general_ci ,
+ADD `gco_total_refund_amount` VARCHAR( 64 ) CHARACTER SET utf8 COLLATE utf8_general_ci,
+ADD `gco_latest_refund_amount` VARCHAR( 64 ) CHARACTER SET utf8 COLLATE utf8_general_ci,
+ADD `lead_source` VARCHAR(255) CHARACTER SET utf8 COLLATE utf8_general_ci,
+ADD `referring_url` VARCHAR(255) CHARACTER SET utf8 COLLATE utf8_general_ci,
+ADD `locale` VARCHAR(6) CHARACTER SET utf8 COLLATE utf8_general_ci,
+ADD `currency_locale` VARCHAR(6) CHARACTER SET utf8 COLLATE utf8_general_ci,
+ADD `txtype` VARCHAR(64) CHARACTER SET utf8 COLLATE utf8_general_ci,
+ADD `cart` BLOB;
+
+And run these to allow for temporary order numbers of greater than the default 14 character field type
+ALTER TABLE `transactions` MODIFY `order_number` varchar(32);
+ALTER TABLE `orderline` MODIFY `order_number` varchar(32);
+
+
+In etc/log_transction, immediately after the 
+[elsif variable MV_PAYMENT_MODE]
+	[calc]
+insert this line: 
+	undef $Session->{payment_result}{MStatus};
+
+and leave
+[elsif variable MV_PAYMENT_MODE] 
+as set (contrary to earlier revisions of this document), but within the same section change the following 
+two instances of
+[var MV_PAYMENT_MODE] to [value mv_payment_route]
+
+Also add these five lines to the end of the section that starts "[import table=transactions ":
+lead_source: [data session source]
+referring_url: [data session referer]
+locale: [scratch mv_locale]
+currency_locale: [scratch mv_currency]
+cart: [calc]uneval($Items)[/calc]
+for use when sending the merchant report and customer receipt emails out. 
+
+Still in etc/log_transaction, find the section that starts "Set order number in values: " and insert
+this just before it:
+[if value mv_order_profile =~ /googlecheckout/]
+[value name=mv_order_number set="[scratch purchaseID]" scratch=1]
+[else]
+and a closing [/else][/if] at the end of that section, just before the 
+"Set order number in session:"
+line. The order number is generated by the module and passed to Google at an early stage, and then
+passed back to Interchange at a later stage. This prevents Interchange generating another order number.
+If your Interchange installation is 5.2.0 or older this line will not exist - set oldic to '1' in 
+the payment route and allow Interchange to generate the order number instead. Note: the initial order number
+uses the username.counter number prefixed with 'GCOtmp', and a normal order number is created and the initial order number
+replaced only when Google reports that the card has been charged. This is to avoid gaps in the order
+number sequence caused by customers abandoning the transaction while at Google. 
+
+
+
+=item Failed atttempts to authorise or charge the buyer's card.
+If the card is declined by the bank then IC will be updated with the new status and a brief email sent
+to the buyer telling him of the fact, and asking him to try another payment method.
+
+
+=item AVS and CV2 risk assessment:
+avs_match_accepted partial|full|none
+
+AVS options and returned values are these:
+Y - Full AVS match (address and postal code)
+P - Partial AVS match (postal code only)
+A - Partial AVS match (address only)
+N - no AVS match
+U - AVS not supported by issuer
+If the route is set to 'full' then, unless AVS is not supported (eg in cards foreign to the country
+doing the processing), a full match is required. Set to 'partial' (the default) for partial match, or
+'none' for no match required.
+
+CV2 values:
+cv2_match_accepted  yes|none
+M - CVN match
+N - No CVN match
+U - CVN not available
+E - CVN error
+If the route is set to 'yes' then the CV2 must match unless it is not available. If set to 'none' then
+a match is not required. Default is 'yes'.
+
+Both of these must be positive according to your rules for the transaction to be charged - if not positive
+then the transaction will be refused and a brief email sent to the prospective buyer to say so.
+
+
+=item Google Analytics
+This page: http://code.google.com/apis/checkout/developer/checkout_analytics_integration.html will tell
+you how to integrate Analytics into the system. This module will pass the data as an 'analyticsdata' 
+value from the checkout form, encoded as UTF-8. 
+
+
+=item Error messages from GCO
+GCO will send error messages with a '<' in the title, which Interchange interprets as a possible attack
+and so immediately stops reading the page and throws the user to the 'violation' page (defined in your
+catalog.cfg as 'SpecialPage ../special_pages/violation' normally, though may be different).
+Insert the following at the top of that page, which will test for the string sent by Google and then
+bounce the user back to the checkout page with a suitable error message. This uses the 'env' UserTag.
+
+[tmp uri][env REQUEST_URI][/tmp]
+ [if  type=explicit compare=`$Scratch->{uri} =~ /%20400%20Bad%20Request%3C\?xml/`]
+[perl]
+ 	$msg = errmsg("GoogleCheckout has encountered an error - if all of your address and shipping entries are correct, please consider using our 'Credit Card Checkout' instead. Our apologies for any inconvenience.");
+	$::Session->{errors}{GoogleCheckout} = $msg;
+[/perl]
+ [bounce href="[area ord/checkout]"]
+ [/if]
+
+=bugs 
+The default CharSet.pm in Interchange 5.6 (and possibly earlier) will fail on GCO's notifications. The
+sympton is that GCO keeps repeating the 'new order notification' as though it has not received one, but
+does not return any errors. Set a variable in your catalog.cfg, thus: 
+Variable	MV_HTTP_CHARSET	UTF-8
+but  be aware that this may break the display of some upper ASCII characters, eg the GBP £ sign (use &pound; instead of £)
+
+=Changelog
+v.0.7.0, 29.01.2009
+	- added locale, currency_locale, and cart fields to transaction tbl
+	- log basket to transaction tbl to be read and inserted back into session for final order route
+	- altered main 'googlecheckout' order route and added new 'gco_final' order route. Replaced previous
+	  method of sending emails with this final route. 
+	- added failsafe logging prior to going to Google, in orders/gco/, file name is 'date.session_id'
+	
+v 0.7.1, May 2009.
+	- changed order number creation to only come after Google reports the card as charged. Initially
+	  uses the tid (from tid.counter) as a temporary order number.
+v0.7.2, May 2009,
+	- updated documentation, simplifed system for co-operating with other payment systems. 
+	  	
+=cut
+
+BEGIN {
+	my $selected;
+		eval {
+			package Vend::Payment;
+			require XML::Simple;
+			require LWP;
+			require MIME::Base64;
+            require HTTP::Request::Common;
+            import HTTP::Request::Common qw(POST);
+			require Net::SSLeay;
+            require Encode;
+            import Encode qw(encode decode);
+            require Data::Dumper;
+			$selected = "XML::Simple and MIME::Base64";
+		};
+
+		$Vend::Payment::Have_Google = 1 unless $@;
+
+	unless ($Vend::Payment::Have_Google) {
+		die __PACKAGE__ . " requires XML::Simple, MIME::Base64";
+	}
+
+use XML::Simple;
+
+::logGlobal("%s v0.7.2 payment module initialised, using %s", __PACKAGE__, $selected) unless $Vend::Quiet;
+
+}
+
+package Vend::Payment;
+
+my ($gcourl,$merchantid,$merchantkey,$gcoserver,$xmlOut, $taxrate, $state, $header, $gcorequest);
+
+sub googlecheckout {
+	my ($opt, $purchaseID, $mv_order_number, $msg, $cart, %result);
+       $gcoserver   = charge_param('googlehost')  || $::Variable->{MV_PAYMENT_HOST} || 'https://checkout.google.com/api/checkout/v2'; # live
+	my $catroot     = charge_param('cat_root') || $::Variable->{CAT_ROOT};
+	my $ordersdir   = charge_param('ordersdir') || 'orders';
+	my $currency    = $::Values->{currency} || charge_param('currency') || 'GBP';	
+	my $editbasketurl = charge_param('edit_basket_url') || $::Variable->{EDIT_BASKET_URL};
+       $editbasketurl =~ s/\.html$//i;
+       $editbasketurl .= ".html?id=$::Session->{id}";
+	my $continueshoppingurl = charge_param('continue_shopping_url') || $::Variable->{CONTINUE_SHOPPING_URL};
+	my $receipturl  = charge_param('receipt_url') || $::Variable->{RECEIPT_URL};
+	my $gcoipn_url  = charge_param('gcoipn_url') || $::Variable->{GCOIPN_URL};
+	my $gcocmd_url  = charge_param('gcocmd_url') || $::Variable->{GCOCMD_URL}; # from IC admin panel, not from GCO
+	my $chargecard  = $::Values->{charge_card} || charge_param('charge_card') || '1';
+	my $basket_expiry = charge_param('basket_expiry') || $::Variable->{BASKET_EXPIRY} || '1 month';
+	my $default_taxrate = $::Values->{default_taxrate} || charge_param('default_taxrate') || '0.00';
+	my $reduced_taxrate = $::Values->{reduced_taxrate} || charge_param('reduced_taxrate') || '0.00';
+	my $taxratefield     = charge_param('taxrate_field') || 'taxrate';
+	my $reduced_taxfield = charge_param('reduced_tax_field') || 'reduced';
+	my $exempt_taxfield = charge_param('exempt_tax_field') || 'exempt';
+	my $tax_included = $::Values->{tax_included} || charge_param('tax_included') || '';
+	my $calculate_included_tax = $::Values->{calculate_included_tax} || charge_param('calculate_included_tax') || '';
+	my $ordernumber  = charge_param('ordernumber') || 'etc/order.number';
+	my $gcocounter   = charge_param('gcocounter') || 'etc/username.counter'; 
+	my $defaultshipmode = charge_param('default_shipmode') || 'upsg';
+	my $defaultcountry  = $::Values->{default_country} || charge_param('default_country') || '';
+	my $defaultstate    = $::Values->{default_state} || charge_param('default_state') || '';
+	my $bypass_auth  = charge_param('bypass_authorization') || '1';
+	my $senderemail  = charge_param('sender_email') ;
+	my $merchantemail = charge_param('merchant_email') || $::Variable->{ORDERS_TO};
+	my $doreceipt    = charge_param('receipt_from_merchant') || '1';
+	my $sendemail    = charge_param('email_auth_charge') || 'charge';
+	my $htmlmail     = charge_param('html_mail') || '';
+	my $mailriskfail = $::Values->{mailriskfail} || charge_param('mail_on_risk_failure') || "Authentication checks failed";
+	my $gcocmd       = $::Values->{gcocmd} || '';
+	my $avsmatch     = charge_param('avs_match_accepted') || 'partial';
+	my $cv2match     = charge_param('cv2_match_accepted') || 'yes';
+	my $checkouturl  = charge_param('checkouturl') || "$::Variable->{SECURE_SERVER}$::Variable->{CGI_URL}/ord/checkout";
+	my $returnurl    = charge_param('returnurl')   || "$::Variable->{SECURE_SERVER}$::Variable->{CGI_URL}/ord/gcoreceipt";
+	   $returnurl    =~ s/\.html$//i;
+	   $returnurl   .= ".html?id=$::Session->{id}";
+	my $diagnose     = $::Values->{gco_diagnose} || charge_param('gco_diagnose') || ''; # set to '1' to have GCO return the XML it receives for diagnostics
+	my $analytics_data = $::Values->{analyticsdata} || '';
+	   $analytics_data = encode('UTF-8', $analytics_data);
+	my $tracking       = charge_param('tracking_script') || ''; 
+	my $without_address = charge_param('without_address') || ''; 
+
+#----------------------------------------------------------------------------------------
+       $merchantid  = charge_param('merchantid')  || $::Variable->{MV_PAYMENT_ID};
+       $merchantkey = charge_param('merchantkey') || $::Variable->{MV_PAYMENT_SECRET};
+       $gcorequest  = charge_param('gcorequest')  || $::Values->{gcorequest} || 'post';
+	   $::Values->{gcorequest} = '';
+			
+	if ($gcorequest eq 'post') {
+    	$gcourl = "$gcoserver/merchantCheckout/Merchant/$merchantid";
+	      }
+	else {
+    	$gcourl = "$gcoserver/request/Merchant/$merchantid";
+	}
+    
+	$gcourl .= "/diagnose" if ($diagnose == '1');
+#::logDebug(":GCO:".__LINE__.": gcourl=$gcourl");
+
+my $gco = new(
+                  merchant_id        => $merchantid,
+                  merchant_key       => $merchantkey,
+                  gco_server         => $gcourl,
+                  currency_supported => $currency
+             );
+
+my (%actual) = map_actual();
+    $actual  = \%actual;
+    $opt     = {};
+#::logDebug(":GCO:".__LINE__." actual map result: " . ::uneval($actual));
+
+#----------------------------------------------------------------------------------------
+# Initial post to GCO
+#----------------------------------------------------------------------------------------
+if ($gcorequest eq 'post') {
+   undef $gcorequest;
+	my $salestax = $::Values->{tax} || Vend::Interpolate::salestax() || '0.00';
+	my $shipmode = $::Values->{mv_shipmode} || charge_param('default_shipmode') || 'upsg';
+	my $shipping = $::Session->{final_shipping} || Vend::Ship::shipping($shipmode) || charge_param('default_shipping') || '0.00';
+	my $handling = $::Values->{handlingtotal} || Vend::Ship::tag_handling() || '';
+       $shipping += $handling;
+	my $shipmsg  = $::Session->{ship_message};
+	my $subtotal = $::Values->{amount} || Vend::Interpolate::subtotal();
+	my $ordertotal = charge_param('amount') || Vend::Interpolate::total_cost();
+	my $defaultcountry = charge_param('defaultcountry');
+	my $defaultstate = charge_param('defaultstate');
+	my $country  = uc($actual->{country});
+       $country  = $defaultcountry unless $country; 
+	my $state    = uc($actual->{state});
+       $state    = $defaultstate unless $state;
+	my $zip_pattern = $actual->{zip} || $::Values->{zip};
+       $zip_pattern =~ /(\S\S\S).*/;
+       $zip_pattern = "$1"."*";
+    my $taxshipping = 'false';
+       $taxshipping = 'true' if ($country =~ /$::Variable->{TAXSHIPPING}/);
+#::logDebug(":GCO:".__LINE__.": shipping=$::Session->{final_shipping}, $shipping; handling=$handling; taxshipping=$::Variable->{TAXSHIPPING}; country=$country; tx=$taxshipping");
+
+if ($salestax == '0') { 
+         $taxrate = '0.00';
+          }
+  elsif ($taxshipping eq 'true') { 
+         $taxrate = ($salestax / ($subtotal + $shipping) || '0');
+          }
+  elsif ($calculate_included_tax == '1') {
+         $taxrate = $default_taxrate;
+          } 
+  else { 
+         $taxrate =  ($salestax / $subtotal || '0');
+}
+#::logDebug(":GCO:".__LINE__.": subtotal=$subtotal; taxrate=$taxrate");
+
+### Check that the currency sent to GCO is the one registered with them, or return to the checkout
+my $user_currency = $::Scratch->{iso_currency_code} || $::Values->{iso_currency_code} || $currency;
+#::logDebug(" ".__LINE__.": user currency = $user_currency, $::Scratch->{iso_currency_code}, $::Values->{iso_currency_code}; currency=$currency"); 
+ if ($user_currency ne $currency) {
+	$msg = errmsg("GoogleCheckout can take only $currency, so please reset the currency option on the page to $currency. Thank you");
+	$::Session->{errors}{GoogleCheckout} = $msg;
+ return();
+}
+
+#::logDebug(":GCO:".__LINE__.": ordertot=$ordertotal; subtot=$subtotal; amount=$::Values->{amount}; tax=$::Values->{tax} - $salestax; invno=$::Values->{inv_no}; zip=$zip_pattern; country=$country");
+
+my ($item, $itm, $basket);
+if (($::Values->{inv_no}) or ($::Values->{digital_delivery})) {
+  $shipmode = 'Digital';
+  $basket = <<EOX;
+   <item>
+    <merchant-item-id>$::Values->{inv_no}</merchant-item-id>
+	<item-name>$::Values->{inv_no}</item-name>
+	<item-description>$::Values->{notes}</item-description>
+	 <quantity>1</quantity>
+	<unit-price currency="$currency">$subtotal</unit-price>
+   </item>
+EOX
+   }
+# TODO: allow for carts other than 'main'
+elsif ($::Carts->{'main'}) {
+	foreach  $item (@{$::Carts->{'main'}}) {
+	    $itm = {
+	    		code         => $item->{'code'},
+				quantity     => $item->{'quantity'},
+				tax_category => Vend::Data::item_field($item,'tax_category'),
+				taxrate      => Vend::Data::item_field($item,$taxrate),
+				description  => Vend::Data::item_description($item),
+				price        => Vend::Data::item_price($item)
+				};
+   if ($itm->{code}){
+# Trailing white space, raw & < > are all 'invalid xml'.
+       $itm->{code} =~ s/\s*$//g;
+       $itm->{code} =~ s/&/&#x26;/g;
+       $itm->{code} =~ s/</&#x3c;/g;
+       $itm->{code} =~ s/>/&#x3e;/g;
+       $itm->{description} =~ s/\s*$//g;
+       $itm->{description} =~ s/&\s/&#x26;\s/g;
+       $itm->{description} =~ s/</&#x3c;/g;
+       $itm->{description} =~ s/>/&#x3e;/g;
+       $itm->{price} =~ s/\s*$//g;
+       $itm->{price} /= (1 + ($itm->{taxrate} || $default_taxrate)) 
+       				if ($calculate_included_tax == '1');
+       $itm->{quantity} =~ s/\s*$//g;
+  
+  if ($itm->{tax_category}) {
+  $basket .= <<EOB;
+   <item>
+    <merchant-item-id>$itm->{code}</merchant-item-id>
+	<item-name>$itm->{code}</item-name>
+	<item-description>$itm->{description}</item-description>
+	<unit-price currency="$currency">$itm->{price}</unit-price>
+    <quantity>$itm->{quantity}</quantity>
+    <tax-table-selector>$itm->{tax_category}</tax-table-selector>
+   </item>
+EOB
+        }
+  else {
+  $basket .= <<EOB;
+   <item>
+    <merchant-item-id>$itm->{code}</merchant-item-id>
+	<item-name>$itm->{code}</item-name>
+	<item-description>$itm->{description}</item-description>
+	<unit-price currency="$currency">$itm->{price}</unit-price>
+    <quantity>$itm->{quantity}</quantity>
+   </item>
+EOB
+      }
+	 }
+   }
+ }
+else {
+  $msg = errmsg("You must pass something to GoogleCheckout");
+	$::Session->{errors}{GoogleCheckout} = $msg;
+	return($msg);
+}
+
+   $orderID = gen_order_id($opt);
+   $::Scratch->{orderID} = $orderID;
+   $::Scratch->{txtype} = 'GCO - PENDING';
+
+# Disable order number creation in log_transaction and create it here instead
+if ($::Values->{inv_no}) {
+   $purchaseID = $::Values->{inv_no};
+      }
+elsif ($::Values->{mv_order_number}){
+  # IC 5.2 and earlier set order number prior to log_transaction
+   $purchaseID = $::Values->{mv_order_number};
+      }
+else{
+# Use temporary number as the initial order number, and only replace upon successful order completion
+    $purchaseID = 'GCOtmp'.Vend::Interpolate::tag_counter("$gcocounter");
+#::logDebug(":GCO:".__LINE__.": purchaseID=$purchaseID;");
+}
+    
+    $::Scratch->{purchaseID} = $purchaseID;
+
+#::logDebug(":GCO:".__LINE__.": txtype=$::Scratch->{txtype};  orderid=$orderID, purchaseid=$purchaseID");
+
+# XML to send
+$xmlOut = <<EOX;
+<?xml version="1.0" encoding="UTF-8"?>
+<checkout-shopping-cart xmlns="http://checkout.google.com/schema/2">
+ <shopping-cart>
+  <merchant-private-data>
+   <merchant-note>$purchaseID</merchant-note>
+  </merchant-private-data>
+   <items>
+EOX
+
+  $xmlOut .= $basket;
+
+  $xmlOut .= <<EOX;
+   </items>
+ </shopping-cart>
+<checkout-flow-support>
+<merchant-checkout-flow-support>
+ <edit-cart-url>$editbasketurl</edit-cart-url>
+  <continue-shopping-url>$continueshoppingurl</continue-shopping-url>
+   <tax-tables>
+	<default-tax-table>
+	 <tax-rules>
+	  <default-tax-rule>
+	  <shipping-taxed>$taxshipping</shipping-taxed>
+	  <rate>$taxrate</rate>
+	  <tax-area>
+EOX
+
+if ($country =~ /US/i) {
+ $xmlOut .= <<EOX;
+ 	 	<us-state-area>
+ 	 	 <state>$state</state>
+ 	 	</us-state-area>
+EOX
+   }
+else {
+  $xmlOut .= <<EOX;
+        <postal-area>
+         <country-code>$country</country-code>
+        </postal-area>
+EOX
+ }
+
+ $xmlOut .= <<EOX;
+	   </tax-area>
+	  </default-tax-rule>
+	 </tax-rules>
+	</default-tax-table>
+    <alternate-tax-tables>
+     <alternate-tax-table standalone="true" name="$reduced_taxfield">
+       <alternate-tax-rules>
+         <alternate-tax-rule>
+           <rate>$reduced_taxrate</rate>
+           <tax-area>
+             <world-area/>
+           </tax-area>
+         </alternate-tax-rule>
+       </alternate-tax-rules>
+     </alternate-tax-table>
+     <alternate-tax-table standalone="true" name="$exempt_taxfield">
+       <alternate-tax-rules/>
+     </alternate-tax-table>
+   </alternate-tax-tables>
+  </tax-tables>
+<shipping-methods>
+ <flat-rate-shipping name="$shipmode">
+  <price currency="$currency">$shipping</price>
+   <shipping-restrictions>
+	<allowed-areas>
+EOX
+
+if ($country =~ /US/i) {
+ $xmlOut .= <<EOX;
+        <us-state-area>
+      	  <state>$state</state>
+	    </us-state-area>
+		<us-zip-area>
+          <zip-pattern>$zip_pattern</zip-pattern>
+        </us-zip-area>
+EOX
+    }
+else {
+  $xmlOut .= <<EOX;
+	 <postal-area>
+	  <country-code>$country</country-code>
+	   <postal-code-pattern>$zip_pattern</postal-code-pattern>
+	 </postal-area>
+EOX
+ }
+  $xmlOut .= <<EOX;
+	</allowed-areas>
+   </shipping-restrictions>
+ </flat-rate-shipping>
+</shipping-methods>
+ <analytics-data>$analytics_data</analytics-data>
+  <parameterized-urls>
+    <parameterized-url url="$returnurl"/>
+  </parameterized-urls>
+</merchant-checkout-flow-support>
+</checkout-flow-support>
+</checkout-shopping-cart>
+EOX
+
+#
+# Write full order to orders/gco/ using gco$date.$session_id file name as failsafe backup in case order 
+# route fails. 
+
+	my $date     = $Tag->time({ body => "%Y%m%d%H%M%S" });
+	my $pagefile = charge_param('report_page') || 'etc/report';
+	my $page     = readfile($pagefile);
+	   $page     = interpolate_html($page) if $page;
+
+	mkdir "$ordersdir/gco", 0775 unless -d "$ordersdir/gco";
+	
+	my $fn = Vend::Util::catfile(
+				"$ordersdir/gco",
+				"gco$date.$::Session->{id}"  
+			);
+   
+    Vend::Util::writefile( $fn, $page )
+				or ::logError("GCO error writing failsafe order $fn: $!");
+	
+#--------------------------------------------------------------------------------
+# Post the basket to GCO and read the redirect URL to which the customer is sent.
+   my $return = sendxml($xmlOut);
+   my $xml    = new XML::Simple();
+   my $xmlin  = $xml->XMLin("$return");
+	  $::Session->{errors}{GoogleCheckout} = $xmlin->{'error-message'}
+				if $xmlin->{'error-message'};
+ 
+    my $redirecturl = $xmlin->{'redirect-url'};
+	my $gco_serial_number = $xmlin->{'serial-number'};
+#::logDebug(":GCO:".__LINE__.": return=$return, redirect=$redirecturl; gcourl=$gcourl;serial number=$gco_serial_number");
+#use Data::Dumper; # for debugging
+# print Dumper($xmlin); # for debugging
+  
+  unless (($xmlin->{'error-message'}) or ($diagnose)) {
+
+$::Tag->tag({ op => 'header', body => <<EOB });
+Status: 302 moved
+Location: $redirecturl
+EOB
+
+# Fake the result so that IC can log the transaction
+   $result{Status}     = 'success';
+   $result{MStatus}    = 'success';
+   $result{'order-id'} = $orderID;
+   
+   return %result;
+      }
+ 
+ }
+
+#----------------------------------------------------------------------------------------
+# Now handle callbacks, eg notification of payment, risk assessment, etc
+#----------------------------------------------------------------------------------------
+
+	elsif ($gcorequest eq callback) {
+
+#### First authenticate the message using the merchantid and merchantkey in the header, then
+#### determine type of callback and respond appropriately.  Apache does not pass HTTP_AUTHORIZATION to
+#### the environment in its default configuration for security reasons, and may need to be recompiled
+
+	my $authdata = $ENV{HTTP_AUTHORIZATION};
+	my ($id, $key, $authed);
+
+  unless ($bypass_auth == '1') {
+	 if (($authdata) and (substr($ENV{HTTP_AUTHORIZATION},0,6) eq 'Basic ')) {
+        my $decoded = decode_base64(substr($ENV{HTTP_AUTHORIZATION},6));
+           if ($decoded =~ /:/) {
+              ($id, $key) = split(/:/, $decoded);
+           		if (($id eq $merchantid) and ($key eq $merchantkey)) {
+                	$authed = 'yes';
+        		      	}
+			    else {
+         			$authed = 'failed';
+		        }
+    	   }
+  	  }
+}
+
+if (($authed eq 'yes') or ($bypass_auth == '1')) {
+
+# Read xml, initialise db table, create new XML object.
+ 	my $xmlIpn = ::http()->{entity};
+# ::logDebug(":GCO:".__LINE__.": xmlIpn=$$xmlIpn");
+	my $db  = dbref('transactions') or die errmsg("cannot open transactions table");
+	my $dbh = $db->dbh() or die errmsg("cannot get handle for tbl 'transactions'");
+    my $sth;
+    
+    my $xml   = new XML::Simple();
+    my $xmlin = $xml->XMLin("$$xmlIpn");
+	my $gco_serial_number = $xmlin->{'serial-number'};
+
+#--- new order notification ---------------------------------------------------------------
+if ($$xmlIpn =~ /new-order-notification/) {
+	my $gco_order_number      = $xmlin->{'google-order-number'}; 
+	my $gco_timestamp         = $xmlin->{'timestamp'}; 	
+	my $gco_fulfillment_state = $xmlin->{'fulfillment-order-state'}; 
+	my $gco_financial_state   = $xmlin->{'financial-order-state'}; 	
+	my $email_allowed         = $xmlin->{'buyer-marketing-preferences'}->{'email-allowed'}; 
+	my $buyers_id             = $xmlin->{'buyer-id'}; 
+	my $total_tax             = $xmlin->{'order-adjustment'}->{'total-tax'}->{'content'}; 
+	my $shipping              = $xmlin->{'order-adjustment'}->{'shipping'}->{'flat-rate-shipping-adjustment'}->{'shipping-cost'}->{'content'}; 
+	my $order_total           = $xmlin->{'order-total'}->{'content'}; 
+	my $mv_order_number       = $xmlin->{'shopping-cart'}->{'merchant-private-data'}->{'merchant-note'};
+	my $company_name          = $xmlin->{'buyer-shipping-address'}->{'company-name'}
+	                               unless ($xmlin->{'buyer-shipping-address'}->{'company-name'} =~ /HASH/);
+	my $buyers_name           = $xmlin->{'buyer-shipping-address'}->{'contact-name'};
+	my $fname                 = $xmlin->{'buyer-shipping-address'}->{'structured-name'}->{'first-name'};
+	my $lname                 = $xmlin->{'buyer-shipping-address'}->{'structured-name'}->{'last-name'};
+	my $address1              = $xmlin->{'buyer-shipping-address'}->{'address1'};
+	my $address2              = $xmlin->{'buyer-shipping-address'}->{'address2'}
+	                               unless ($xmlin->{'buyer-shipping-address'}->{'address2'} =~ /HASH/);
+	my $city                  = $xmlin->{'buyer-shipping-address'}->{'city'};
+	my $state                 = $xmlin->{'buyer-shipping-address'}->{'region'}
+	                               unless ($xmlin->{'buyer-shipping-address'}->{'region'} =~ /HASH/);
+	my $postal_code           = $xmlin->{'buyer-shipping-address'}->{'postal-code'}
+	                               unless ($xmlin->{'buyer-shipping-address'}->{'postal-code'} =~ /HASH/);
+	my $country               = $xmlin->{'buyer-shipping-address'}->{'country-code'};
+	my $phone                 = $xmlin->{'buyer-shipping-address'}->{'phone'}
+	                               unless ($xmlin->{'buyer-shipping-address'}->{'phone'} =~ /HASH/);
+	my $fax                   = $xmlin->{'buyer-shipping-address'}->{'fax'}
+	                               unless ($xmlin->{'buyer-shipping-address'}->{'fax'} =~ /HASH/);
+	my $email                 = $xmlin->{'buyer-shipping-address'}->{'email'};
+	my $b_company_name        = $xmlin->{'buyer-billing-address'}->{'company-name'}
+	                               unless ($xmlin->{'buyer-billing-address'}->{'company-name'} =~ /HASH/);
+	my $b_buyers_name         = $xmlin->{'buyer-billing-address'}->{'contact-name'};
+	my $b_fname               = $xmlin->{'buyer-billing-address'}->{'structured-name'}->{'first-name'};
+	my $b_lname               = $xmlin->{'buyer-billing-address'}->{'structured-name'}->{'last-name'};
+	my $b_address1            = $xmlin->{'buyer-billing-address'}->{'address1'};
+	my $b_address2            = $xmlin->{'buyer-billing-address'}->{'address2'}
+	                               unless ($xmlin->{'buyer-billing-address'}->{'address2'} =~ /HASH/);
+	my $b_city                = $xmlin->{'buyer-billing-address'}->{'city'};
+	my $b_state               = $xmlin->{'buyer-billing-address'}->{'region'}
+	                               unless ($xmlin->{'buyer-billing-address'}->{'region'} =~ /HASH/);
+	my $b_postal_code         = $xmlin->{'buyer-billing-address'}->{'postal-code'}
+	                               unless ($xmlin->{'buyer-billing-address'}->{'postal-code'} =~ /HASH/);
+	my $b_country             = $xmlin->{'buyer-billing-address'}->{'country-code'};
+	my $b_phone               = $xmlin->{'buyer-billing-address'}->{'phone'}
+	                               unless ($xmlin->{'buyer-billing-address'}->{'phone'} =~ /HASH/);
+
+   	   $buyers_name =~ /(\w+)\s+(\D+)/;
+	   $fname = $1 if ($fname =~ /HASH/);
+	   $lname = $2 if ($lname =~ /HASH/);
+   	   $b_buyers_name =~ /(\w+)\s+(\D+)/;
+	   $b_fname = $1 if ($b_fname =~ /HASH/);
+	   $b_lname = $2 if ($b_lname =~ /HASH/);
+       
+       $postal_code =~ /^(\S\S\S).*/;
+    my $postal_code_short = $1;
+ 
+# Add IC order number to GCO admin panel
+    my $xmlOut = <<EOX;
+<?xml version="1.0" encoding="UTF-8"?>
+<add-merchant-order-number xmlns="http://checkout.google.com/schema/2" google-order-number="$gco_order_number">
+    <merchant-order-number>$mv_order_number</merchant-order-number>
+</add-merchant-order-number>
+EOX
+		sendxml($xmlOut);
+
+#::logDebug(":GCO:".__LINE__.": gsn=$gco_serial_number, gon=$gco_order_number, shipping=$shipping,  fname=$fname, lname=$lname, mvon=$mv_order_number");
+# Update IC db - update total_cost here as well, in case of penny differences in rounding methods.
+	  $sth = $dbh->prepare("UPDATE transactions SET fname='$fname',lname='$lname',address1='$address1',address2='$address2',city='$city',state='$state',zip='$postal_code',country='$country',phone_day='$phone',fax='$fax',email='$email',company='$company_name', b_fname='$fname',b_lname='$lname',b_address1='$address1',b_address2='$address2',b_city='$city',b_state='$state',b_zip='$postal_code',b_country='$country',b_phone='$phone',b_company='$company_name',total_cost='$order_total', salestax='$total_tax',shipping='$shipping', gco_order_number='$gco_order_number',txtype='GCO - $gco_financial_state',gco_fulfillment_state='$gco_fulfillment_state',gco_serial_number='$gco_serial_number',gco_buyers_id='$buyers_id',gco_timestamp='$gco_timestamp' WHERE order_number='$mv_order_number'");
+      $sth->execute() or die errmsg("Cannot update transactions tbl for gco '$mv_order_number'");
+    
+    }
+
+#--- update to order ---------------------------------------------------------------------------------------
+elsif ($$xmlIpn =~ /order-state-change-notification/) {
+	my $gco_serial_number     = $xmlin->{'serial-number'};
+	my $gco_order_number      = $xmlin->{'google-order-number'}; 
+	my $gco_timestamp         = $xmlin->{'timestamp'}; 
+	my $gco_fulfillment_state = $xmlin->{'new-fulfillment-order-state'}; 
+	my $gco_financial_state   = $xmlin->{'new-financial-order-state'}; 
+	
+	   $sth = $dbh->prepare("SELECT total_cost,email,txtype,order_number FROM transactions WHERE gco_order_number='$gco_order_number'") or die errmsg("Cannot select from transactions tbl for $gco_order_number");
+       $sth->execute() or die errmsg("Cannot get data from transactions tbl");
+    my @d = $sth->fetchrow_array;
+    my $order_total = $d[0];
+    my $email       = $d[1];
+    my $txtype      = $d[2];
+    my $old_tid     = $d[3];
+
+	unless ($txtype =~ /GCO - CHARGED/i) {
+	   if ($gco_financial_state =~ /CHARGED/i) {
+	   $new_order_no  = Vend::Interpolate::tag_counter("$ordernumber") ; 
+	   $sth = $dbh->prepare("UPDATE transactions SET code='$new_order_no', order_number='$new_order_no', txtype='GCO - $gco_financial_state',gco_fulfillment_state='$gco_fulfillment_state',gco_timestamp='$gco_timestamp' WHERE gco_order_number='$gco_order_number'");
+	   $stho = $dbh->prepare("UPDATE orderline SET code=replace(code, '$old_tid', '$new_order_no'), order_number='$new_order_no' WHERE order_number='$old_tid'");
+	   $stho->execute() or die errmsg("Cannot update transactions tbl for gco '$gco_order_number'");
+	   			}
+	   	else {
+       $sth = $dbh->prepare("UPDATE transactions SET txtype='GCO - $gco_financial_state', gco_fulfillment_state='$gco_fulfillment_state',gco_timestamp='$gco_timestamp' WHERE gco_order_number='$gco_order_number'");
+       		}
+       $sth->execute() or die errmsg("Cannot update transactions tbl for gco '$gco_order_number'");
+#::logDebug(":GCO:".__LINE__.": gco_finstate=$gco_financial_state; txtype=$txtype; neworderno=$new_order_no; pID=$purchaseID");	   
+       }
+
+	my ($mailout, $finstatus);
+
+	if ($gco_financial_state =~ /PAYMENT_DECLINED/i) {
+        $mailout = <<EOM;
+Card payment for Google Order number $gco_order_number from $::Variable->{COMPANY}, $order_total, was
+declined by your bank. Please use an alternative means of payment if you wish to proceed with this order.
+EOM
+  	$finstatus = "declined by your bank";
+  	}
+	elsif ($gco_financial_state =~ /CANCELLED/i) {
+  		$mailout = <<EOM;
+Google Order number $gco_order_number from $::Variable->{COMPANY} has been cancelled.
+EOM
+  	$finstatus = "cancelled";
+  	}
+
+	if ($gco_financial_state =~ /PAYMENT_DECLINED|CANCELLED/i) {
+        $::Tag->email({ to => "$email", from => "$senderemail", reply => "$senderemail", extra => "Bcc: $merchantemail",
+                subject => "Google order $gco_order_number has been $finstatus",
+                body => "$mailout\n"
+             });
+        }
+  }
+
+
+#--- risk notification ---------------------------------------------------------------------------------------
+elsif ($$xmlIpn =~ /risk-information-notification/) {
+	my $gco_serial_number = $xmlin->{'serial-number'};
+	my $gco_order_number  = $xmlin->{'google-order-number'}; 
+	my $gco_timestamp     = $xmlin->{'timestamp'}; 
+	my $gco_protection    = $xmlin->{'risk-information'}->{'eligible-for-protection'};
+	my $gco_avs_response  = $xmlin->{'risk-information'}->{'avs-response'};
+	my $gco_cvn_response  = $xmlin->{'risk-information'}->{'cvn-response'};	
+	my $gco_cc_number     = $xmlin->{'risk-information'}->{'partial-cc-number'};
+	my $gco_account_age   = $xmlin->{'risk-information'}->{'buyer-account-age'};
+	my $gco_buyers_ip     = $xmlin->{'risk-information'}->{'ip-address'};
+
+	   $sth = $dbh->prepare("UPDATE transactions SET gco_avs_response='$gco_avs_response',gco_cvn_response='$gco_cvn_response',gco_protection='$gco_protection',gco_cc_number='$gco_cc_number', gco_timestamp='$gco_timestamp' WHERE gco_order_number='$gco_order_number'");
+       $sth->execute() or die errmsg("Cannot update transactions tbl for gco '$gco_order_number'");
+
+# Assess risk, and if OK then optionally tell GCO to charge the card; and send out emails.
+    my ($process_order, $avs, $cv2);
+  if (($avsmatch) eq 'full' and ($gco_avs_response =~ /Y|U/i)) {
+   		$avs = 'pass';
+   		}
+	elsif (($avsmatch) eq 'partial' and ($gco_avs_response !~ /N/i)) {
+   		$avs = 'pass';
+ 		}
+	elsif ($avsmatch eq 'none') {
+   		$avs = 'pass';
+   }
+
+  if (($cv2match) eq 'yes' and ($gco_cvn_response !~ /N/i)) {
+   		$cv2 = 'pass';
+ 		}
+	elsif ($cv2match eq 'none') {
+   		$cv2 = 'pass';
+  }
+ 
+  if (($avs eq 'pass') and ($cv2 eq 'pass')) {
+   		if ($chargecard =~ /1|y/) { 
+    # Tell Google to charge the card
+       $sth = $dbh->prepare("SELECT total_cost FROM transactions WHERE gco_order_number='$gco_order_number'") or die errmsg("Cannot select from transactions tbl for $gco_order_number");
+       $sth->execute() or die errmsg("Cannot get data from transactions tbl");
+    my $order_total = $sth->fetchrow();
+
+    my $xmlOut = <<EOX;
+<?xml version="1.0" encoding="UTF-8"?>
+<charge-order xmlns="http://checkout.google.com/schema/2" google-order-number="$gco_order_number">
+    <amount currency="$currency">$order_total</amount>
+</charge-order>
+EOX
+	 sendxml($xmlOut) ;
+    		}
+        }
+ else  {
+# Risk assessment fails to meet rules
+  	   $::Tag->email({ to => "$email", from => "$senderemail", reply => "$sendermail", extra => "Bcc: $merchantemail",
+                subject => "Google order $gco_order_number declined\n\n",
+                body => "$mailriskfail\n"
+             });
+        }
+    }
+
+#--- charge amount ----------------------------------------------------------------------------------------
+elsif ($$xmlIpn =~ /charge-amount-notification/) {
+	my $gco_serial_number        = $xmlin->{'serial-number'};
+	my $gco_order_number         = $xmlin->{'google-order-number'}; 
+	my $gco_timestamp            = $xmlin->{'timestamp'}; 
+	my $gco_latest_charge_amount = $xmlin->{'latest-charge-amount'}->{'content'};
+	my $gco_total_charge_amount  = $xmlin->{'total-charge-amount'}->{'content'};
+
+       $sth = $dbh->prepare("SELECT total_cost,email,order_number,fname,lname,company,address1,address2,city,state,zip,country,phone_day,fax,b_fname,b_lname,b_company,b_address1,b_address2,b_city,b_state,b_zip,b_country,shipmode,handling,order_date,lead_source,referring_url,txtype,locale,currency_locale,cart FROM transactions WHERE gco_order_number='$gco_order_number'") or die errmsg("Cannot select from transactions tbl for $gco_order_number");
+       $sth->execute() or die errmsg("Cannot get data from transactions tbl");
+    my @d = $sth->fetchrow_array;
+    my $order_total = $::Values->{order_total} = $d[0];
+    my $email = $::Values->{email} = $d[1];
+    my $mv_order_number = $::Values->{mv_order_number} = $d[2];
+    my $fname = $::Values->{fname} = $d[3];
+    my $lname = $::Values->{lname} = $d[4];
+    my $company = $::Values->{company} = $d[5];
+    my $address1 = $::Values->{address1} = $d[6];
+    my $address2 = $::Values->{address2} = $d[7];
+    my $city = $::Values->{city} = $d[8];
+    my $state = $::Values->{state} = $d[9];
+    my $zip = $::Values->{zip} = $d[10];
+    my $country = $::Values->{country} = $d[11];
+    my $phone_day = $::Values->{phone_day} = $d[12];
+    my $fax = $::Values->{fax} = $d[13];
+    my $b_fname = $::Values->{b_fname} = $d[14];
+    my $b_lname = $::Values->{b_lname} = $d[15];
+    my $b_company = $::Values->{b_company} = $d[16];
+    my $b_address1 = $::Values->{b_address1} = $d[17];
+    my $b_address2 = $::Values->{b_address2} = $d[18];
+    my $b_city = $::Values->{b_city} = $d[19];
+    my $b_state = $::Values->{b_state} = $d[20];
+    my $b_zip = $::Values->{b_zip} = $d[21];
+    my $b_country = $::Values->{b_country} = $d[22];
+    my $shipmode = $::Values->{shipmode} = $d[23];
+    my $handling = $::Values->{handling} = $d[24];
+    my $order_date = $::Values->{order_date} = $d[25];
+    my $lead_source = $::Session->{lead_source} = $d[26];
+    my $referring_url = $::Session->{referer} = $d[27];
+	my $txtype = $::Values->{txtype} = $d[28];
+	my $mv_locale = $d[29];
+	my $mv_currency = $d[30];
+	my $cart = $d[31];
+
+       $cart =~ s/\"/\'/g;
+       $cart =~ s/\\//;
+	   @cart = eval($cart); 
+#::logDebug(":GCO:".__LINE__.": cart=$cart");	
+	   
+	   $::Values->{mv_payment} = 'GoogleCheckout';
+	   $::Values->{gco_order_number} = $gco_order_number;
+	   $::Session->{values}->{iso_currency_code} = $currency;
+	   $::Session->{scratch}->{mv_locale} = $mv_locale;
+	   $::Session->{scratch}->{mv_currency} = $mv_currency || $locale;
+ 
+ # Check that the order has not already been charged, as Google sometimes send extra IPNs when they shouldn't.
+	unless ($txtype =~ /GCO - CHARGED/i) {
+ 	   $sth = $dbh->prepare("UPDATE transactions SET order_number='$purchaseID', gco_latest_charge_amount='$gco_latest_charge_amount',gco_total_charge_amount='$gco_total_charge_amount',gco_timestamp='$gco_timestamp' WHERE gco_order_number='$gco_order_number'");
+       $sth->execute() or die errmsg("Cannot update transactions tbl for gco '$gco_order_number'");
+       }
+ 
+# run custom final route which cascades 'copy_user' and 'main_entry', ie no receipt page.
+     	Vend::Order::route_order("gco_final", @cart) if @cart;
+
+    }
+
+#--- chargeback amount -------------------------------------------------------------------------------------
+elsif ($$xmlIpn =~ /chargeback-amount-notification/) {
+	my $gco_serial_number            = $xmlin->{'serial-number'};
+	my $gco_order_number             = $xmlin->{'google-order-number'}; 
+	my $gco_timestamp                = $xmlin->{'timestamp'}; 
+	my $gco_latest_chargeback_amount = $xmlin->{'latest-chargeback-amount'}->{'content'};
+	my $gco_total_chargeback_amount  = $xmlin->{'total-chargeback-amount'}->{'content'};
+
+	   $sth = $dbh->prepare("UPDATE transactions SET txtype='CHARGEBACK', gco_latest_chargeback_amount='$gco_latest_chargeback_amount',gco_total_chargeback_amount='$gco_total_chargeback_amount',gco_timestamp='$gco_timestamp' WHERE gco_order_number='$gco_order_number'");
+   	   $sth->execute() or die errmsg("Cannot update transactions tbl for gco '$gco_order_number'");
+
+	my $mailchargeback = <<EOM;
+Order $gco_order_number has had a chargeback of $gco_latest_chargeback_amount on $date, making the total
+chargeback for this order $gco_total_chargeback_amount.
+EOM
+  	$::Tag->email({ to => "$merchantemail", from => "$senderemail", reply => "$sendermail",
+                subject => "Google order $gco_order_number has had a CHARGEBACK",
+                body => "$mailchargeback\n",
+             });
+
+    }
+
+
+#--- refund amount -------------------------------------------------------------------------------------
+elsif ($$xmlIpn =~ /refund-amount-notification/) {
+	my $gco_serial_number        = $xmlin->{'serial-number'};
+	my $gco_order_number         = $xmlin->{'google-order-number'}; 
+	my $gco_timestamp            = $xmlin->{'timestamp'}; 
+	my $gco_latest_refund_amount = $xmlin->{'latest-refund-amount'}->{'content'};
+	my $gco_total_refund_amount  = $xmlin->{'total-refund-amount'}->{'content'};
+
+	   $sth = $dbh->prepare("UPDATE transactions SET txtype='REFUND', gco_latest_refund_amount='$gco_latest_refund_amount',gco_total_refund_amount='$gco_total_refund_amount',gco_timestamp='$gco_timestamp' WHERE gco_order_number='$gco_order_number'");
+       $sth->execute() or die errmsg("Cannot update transactions tbl for gco '$gco_order_number'");
+
+	my $mailrefund = <<EOM;
+Order $gco_order_number has been refunded for $gco_latest_refund_amount on $date, making the total
+refund for this order $gco_total_refund_amount.
+EOM
+  	$::Tag->email({ to => "$merchantemail", from => "$senderemail", reply => "$sendermail",
+                subject => "Google order $gco_order_number has been refunded",
+                body => "$mailrefund\n",
+             });
+     }
+  
+ }
+
+#===================================================================================================
+# Now deal with any commands: charge card, ship, refund etc, which might come through an admin panel
+#---------------------------------------------------------------------------------------------------
+
+elsif ($gcorequest eq 'command') {
+
+	my $gco_order_number = $::Values->{gco_order_number};
+	my $mv_order_number  = $::Values->{mv_order_number};
+	my $amount           = $::Values->{gco_amount};
+	my $reason           = $::Values->{gco_reason};
+	my $carrier          = $::Values->{gco_shipping_company};
+       $carrier = 'Other' unless ($carrier =~ /DHL|FedEx|UPS|USPS/i);
+	my $tracking_number  = $::Values->{tracking_number};
+	my $send_email       = $::Values->{email_text};
+
+#--- charge order -----------------------------------------------------------------------------
+if ($gcocmd =~ /charge/) {
+    $xmlOut = <<EOX;
+<?xml version="1.0" encoding="UTF-8"?>
+<charge-order xmlns="http://checkout.google.com/schema/2" google-order-number="$gco_order_number">
+    <amount currency="$currency">$amount</amount>
+</charge-order>
+EOX
+   
+	my $return = sendxml($xmlOut);
+	my $xml    = new XML::Simple();
+	my $xmlin  = $xml->XMLin("$return");
+	   $::Session->{payment_result}{Terminal} = 'success' unless ($xmlin->{'error-message'});	
+       $::Session->{errors}{GoogleCheckout} = $xmlin->{'error-message'}	if ($xmlin->{'error-message'});
+    }
+
+#--- add Interchange order number --------------------------------------------------------------
+elsif ($gcocmd =~ /add_order_number/) {
+       $xmlOut = <<EOX;
+<?xml version="1.0" encoding="UTF-8"?>
+<add-merchant-order-number xmlns="http://checkout.google.com/schema/2" google-order-number="$gco_order_number">
+    <merchant-order-number>$mv_order_number</merchant-order-number>
+</add-merchant-order-number>
+EOX
+
+	my $return = sendxml($xmlOut);
+	my $xml    = new XML::Simple();
+	my $xmlin  = $xml->XMLin("$return");
+	   $::Session->{payment_result}{Terminal} = 'success' unless ($xmlin->{'error-message'});	
+   	   $::Session->{errors}{GoogleCheckout} = $xmlin->{'error-message'} if ($xmlin->{'error-message'});
+    }
+
+#--- refund order ------------------------------------------------------------------------------
+elsif ($gcocmd =~ /refund/) {
+       $xmlOut = <<EOX;
+<?xml version="1.0" encoding="UTF-8"?>
+<refund-order xmlns="http://checkout.google.com/schema/2" google-order-number="$gco_order_number">
+    <amount currency="$currency">$amount</amount>
+    <reason>$::Values->{reason}</reason>
+</refund-order>
+EOX
+   
+   my $return = sendxml($xmlOut);
+   my $xml   = new XML::Simple();
+   my $xmlin = $xml->XMLin("$return");
+	  $::Session->{payment_result}{Terminal} = 'success' unless ($xmlin->{'error-message'});	
+   	  $::Session->{errors}{GoogleCheckout} = $xmlin->{'error-message'} if ($xmlin->{'error-message'});
+    }
+
+#--- cancel order -------------------------------------------------------------------------------
+elsif ($gcocmd =~ /cancel/) {
+       $xmlOut = <<EOX;
+<?xml version="1.0" encoding="UTF-8"?>
+<cancel-order xmlns="http://checkout.google.com/schema/2" google-order-number="$gco_order_number">
+    <reason>$reason</reason>
+</cancel-order>
+EOX
+      
+   my $return = sendxml($xmlOut);
+   my $xml    = new XML::Simple();
+   my $xmlin  = $xml->XMLin("$return");
+	  $::Session->{payment_result}{Terminal} = 'success' unless ($xmlin->{'error-message'});	
+   	  $::Session->{errors}{GoogleCheckout} = $xmlin->{'error-message'} if ($xmlin->{'error-message'});
+    }
+
+#--- authorise order ----------------------------------------------------------------------------
+elsif ($gcocmd =~ /authorise/) {
+       $xmlOut = <<EOX;
+<?xml version="1.0" encoding="UTF-8"?>
+<authorize-order xmlns="http://checkout.google.com/schema/2" google-order-number="$gco_order_number"/>      
+EOX
+   
+   my $return = sendxml($xmlOut);
+   my $xml    = new XML::Simple();
+   my $xmlin  = $xml->XMLin("$return");
+	  $::Session->{payment_result}{Terminal} = 'success' unless ($xmlin->{'error-message'});	
+   	  $::Session->{errors}{GoogleCheckout} = $xmlin->{'error-message'} if ($xmlin->{'error-message'});
+   
+   }
+
+#--- archive order ------------------------------------------------------------------------------
+elsif ($gcocmd =~ /archive/) {
+       $xmlOut = <<EOX;
+<?xml version="1.0" encoding="UTF-8"?>
+<archive-order xmlns="http://checkout.google.com/schema/2" google-order-number="$gco_order_number" />
+EOX
+   
+   my $return = sendxml($xmlOut);
+   my $xml    = new XML::Simple();
+   my $xmlin  = $xml->XMLin("$return");
+	  $::Session->{payment_result}{Terminal} = 'success' unless ($xmlin->{'error-message'});	
+   	  $::Session->{errors}{GoogleCheckout} = $xmlin->{'error-message'} if ($xmlin->{'error-message'});
+      
+    }
+
+#--- add tracking data --------------------------------------------------------------------------
+elsif ($gcocmd =~ /add_tracking/) {
+       $xmlOut = <<EOX;
+<?xml version="1.0" encoding="UTF-8"?>
+<add-tracking-data xmlns="http://checkout.google.com/schema/2" google-order-number="$gco_order_number">
+    <tracking-data>
+        <carrier>$carrier</carrier>
+        <tracking-number>$tracking_number</tracking-number>
+    </tracking-data>
+</add-tracking-data>
+EOX
+     
+   my $return = sendxml($xmlOut);
+   my $xml    = new XML::Simple();
+   my $xmlin  = $xml->XMLin("$return");
+	  $::Session->{payment_result}{Terminal} = 'success' unless ($xmlin->{'error-message'});	
+	  $::Session->{errors}{GoogleCheckout} = $xmlin->{'error-message'} if ($xmlin->{'error-message'});
+         
+    }
+
+#--- deliver order --------------------------------------------------------------------
+elsif ($gcocmd =~ /deliver/) {
+       $xmlOut = <<EOX;
+<?xml version="1.0" encoding="UTF-8"?>
+<deliver-order xmlns="http://checkout.google.com/schema/2" google-order-number="$gco_order_number">
+    <tracking-data>
+        <carrier>$carrier</carrier>
+        <tracking-number>$tracking_number</tracking-number>
+    </tracking-data>
+    <send-email>$send_email</send-email>
+</deliver-order>
+EOX
+   
+   my $return = sendxml($xmlOut);
+   my $xml    = new XML::Simple();
+   my $xmlin  = $xml->XMLin("$return");
+	  $::Session->{payment_result}{Terminal} = 'success' unless ($xmlin->{'error-message'});	
+	  $::Session->{errors}{GoogleCheckout} = $xmlin->{'error-message'} if ($xmlin->{'error-message'});
+       
+    }
+
+#--- end of admin panel commands ----------------------------------------------------------------------------
+    
+    }
+  }
+}
+
+#------------------
+sub new {
+  my (%args) = @_;
+  my $class = Vend::Payment::GoogleCheckout;
+  my $self;
+     $self->{__merchant_id}        = $args{merchant_id};
+     $self->{__merchant_key}       = $args{merchant_key};
+     $self->{__base_gco_server}    = $args{gco_server};
+     $self->{__currency_supported} = $args{currency_supported} || 'USD';
+     $self->{__xml_schema}         = $args{xml_schema} || 'http://checkout.google.com/schema/2';
+     $self->{__xml_version}        = $args{xml_version} || '1.0';
+     $self->{__xml_encoding}       = $args{xml_encoding} || 'UTF-8';
+# ::logDebug(":GCO:".__LINE__.": class=$class; id=$self->{__merchant_id}; key=$self->{__merchant_key} server=$self->{__base_gco_server}");
+  return bless $self => $class;
+}
+
+sub sendxml {
+use MIME::Base64;
+  my $xmlOut = shift;
+  my $agent  = LWP::UserAgent->new;
+  my $data   = "$merchantid:$merchantkey";
+  my $signature = encode_base64($data, "");
+     $header  = HTTP::Headers->new;
+     $header->header('Authorization' => "Basic " . $signature);
+     $header->header('Content-Type'  => "application/xml; charset=UTF-8");
+     $header->header('Accept'        => "application/xml");
+  my $request = HTTP::Request->new(POST => $gcourl, $header, $xmlOut);
+  my $response = $agent->request($request);
+#::logDebug(":GCO:".__LINE__.": sendxml: gcourl=$gcourl\nxmlOut=$xmlOut");
+  return $response->content;
+}
+
+1;
diff --git a/lib/Vend/Payment/PaypalExpress.pm b/lib/Vend/Payment/PaypalExpress.pm
index 51daa46..704f5e9 100644
--- a/lib/Vend/Payment/PaypalExpress.pm
+++ b/lib/Vend/Payment/PaypalExpress.pm
@@ -54,7 +54,7 @@ Require module Vend::Payment::PaypalExpress. Ensure that your perl installation
 listed above and their pre-requisites.
 
 Logon to your Paypal Business (not Personal) account and go to 'Profile' -> 'API access' ->
-'Request API Credential' -> 'Signature'. This will generate a user id, password and signature.
+'Request API Credentials' -> 'Signature'. This will generate a user id, password and signature.
 
 Add to catalog.cfg all marked 'required', optionally the others:
 Route  paypalexpress id   xxx  (required_
@@ -75,7 +75,7 @@ and similarly the cancelurl may be set in the page.
 
 To have Paypal co-operate with your normal payment service provider, eg Authorizenet, do the following:
 
-Deactivate the MV_PAYMENT_MODE variable in catalog.cfg and products/variable.txt.
+Leave the MV_PAYMENT_MODE variable in catalog.cfg and products/variable.txt set to your normal payment processor.
 
 Add to etc/profiles.order:
 __NAME__                       paypalexpress
@@ -89,62 +89,36 @@ email=email
 &final = yes
 &setcheck = payment_method paypalexpress
 __END__
+or, if you want to use Paypal as a 'Buy now' button without taking any customer details, then omit the
+__COMMON_ORDER_PROFILE__ and the two 'email=...' lines above. 
 
-Within the 'credit_card' section of etc/profiles.order change both instances of
-"MV_PAYMENT_MODE" to "MV_PAYMENT_BANK"
+Within the 'credit_card' section of etc/profiles.order leave "MV_PAYMENT_MODE" as set,
 and add
-&set=psp __MV_PAYMENT_BANK__
+&set=psp __MV_PAYMENT_PSP__
 &set=mv_payment_route authorizenet
-(or your preferred gateway) as the last entries in the section.
+(or your preferred gateway instead of authorizenet) as the last entries in the section.
 
 and then add
-Variable MV_PAYMENT_BANK "foo"
+Variable MV_PAYMENT_PSP "foo"
 to catalog.cfg, where "foo" is the name of your gateway or acquirer, formatted as you want it to appear
 on the receipt. Eg, "Bank of America" (rather than boa), "AuthorizeNet" (rather than authorizenet).
 
-In etc/log_transction, change
-[elsif variable MV_PAYMENT_MODE] to [elsif value mv_order_profile eq credit_card]
-and within the same section change the following two instances of
-[var MV_PAYMENT_MODE] to [value mv_payment_route]
-
-Just after the credit_card section, add the following:
-
-[elsif value mv_order_profile eq paypalexpress]
-	[calc]
-		return if $Scratch->{tmp_total} == $Scratch->{tmp_remaining};
-		my $msg = sprintf "Your Paypal account was charged %.2f", $Scratch->{tmp_remaining};
-		$Scratch->{pay_cert_total} = $Scratch->{tmp_total} - $Scratch->{tmp_remaining};
-		$Scratch->{charge_total_message} = $msg;
-		return "Paypal will be charged $Scratch->{tmp_remaining}";
-	[/calc]
-	Charging with payment mode=paypalexpress
-	[tmp name="charge_succeed"][charge route="paypalexpress" pprequest="dorequest" amount="[scratch tmp_remaining]" order_id="[value mv_transaction_id]"][/tmp]
-	[if scratch charge_succeed]
-	[then]
-	[set do_invoice]1[/set]
-	[set do_payment]1[/set]
-	Real-time charge succeeded. ID=[data session payment_id] amount=[scratch tmp_remaining]
-	[/then]
-	[else]
-	Real-time charge FAILED. Reason: [data session payment_error]
+In etc/log_transction, immediately after the 
+[elsif variable MV_PAYMENT_MODE]
 	[calc]
-		for(qw/
-				charge_total_message
-				pay_cert_total
-		/)
-		{
-			delete $Scratch->{$_};
-		}
-		die errmsg(
-				"Real-time charge failed. Reason: %s\n",
-				errmsg($Session->{payment_error}),
-			);
-	[/calc]
-	[/else]
-	[/if]
-[/elsif]
-This runs the final Paypal charge route, handles deductions for gift certificates from the amount
-payable, and handles errors in the same way as the previous credit_card section does.
+insert this line: 
+	undef $Session->{payment_result}{MStatus};
+
+and leave
+[elsif variable MV_PAYMENT_MODE] 
+as set (contrary to previous revisions of this document) but within the same section change the following 
+two instances of [var MV_PAYMENT_MODE] to [value mv_payment_route]. In particular, the setting inside the
+[charge route="..] line will specify which payment processor is used for each particular case, and you
+need to further modify this line so that it ends up like this:
+	[tmp name="charge_succeed"][charge route="[value mv_payment_route]" pprequest="dorequest" amount="[scratch tmp_remaining]" order_id="[value mv_transaction_id]"][/tmp]
+If the value of 'mv_payment_route' is set to 'paypalexpress', then this is the one that is run. It is only
+called via log_transaction after the customer has returned from Paypal and clicks the 'final' pay button, 
+hence this is where the final 'pprequest=dorequest' value is sent. 
 
 Add into the end of the "[import table=transactions type=LINE continue=NOTES no-commit=1]" section
 of etc/log_transaction:
@@ -153,23 +127,21 @@ psp: [value psp]
 pptransactionid: [calc]$Session->{payment_result}{TransactionID}[/calc]
 pprefundtransactionid: [calc]$Session->{payment_result}{RefundTransactionID}[/calc]
 ppcorrelationid: [calc]$Session->{payment_result}{CorrelationID};[/calc]
+pppayerstatus: [value payerstatus]
+ppaddressstatus: [value address_status]
 
-and add these 4 new columns into your transactions table.
+and add these 6 new columns into your transactions table as type varchar(256).
 You will have records of which transactions went through which payment service providers, as well
-as Paypal's returned IDs. The CorrelationID is the one you need in any dispute with them.
+as Paypal's returned IDs. The CorrelationID is the one you need in any dispute with them. The payerstatus
+and addressstatus results may be useful in the order fulfillment process. 
 
-Add these lines into the body of the 'Go to Paypal' button that sends the customer to Paypal.
-      [button
-        normal stuff
-            ]
+Add these lines into the body of the 'submit' button that sends the customer to Paypal.
           [run-profile name=paypalexpress]
           [if type=explicit compare="[error all=1 show_var=1 keep=1]"]
           mv_nextpage=ord/checkout
           [/if]
           [charge route="paypalexpress" pprequest="setrequest"]
           mv_todo=return
-       [/button]
-Note that 'mv_todo' is return, not submit. 
 
 Create a page 'ord/paypalgetrequest.html', and make it the target of the returnURL from Paypal:
 [charge route="paypalexpress" pprequest="getrequest"]
@@ -194,18 +166,16 @@ number by default, so you may need to adjust your order profiles to compensate.
 Also note that Paypal requires the user to have cookies enabled, and if they're not will return an error page with no 
 indication of the real problem. You may want to warn users of this. 
 
-The flow is: the first button for Paypal goes to the 'paypalsetrequest' page, which sends a request
-to Paypal to initialise the transaction and gets a token back in return. If Paypal fails to send back
-a token, then the module refreshes that page with an error message suggesting that the customer should
-use your normal payment service provider and shows the cards that you accept. Once the token is read, then
-your customer is taken to Paypal to login and choose his payment method. Once that is done, he returns
-to us and hits the 'paypalgetrequest' page. This gets his full address as held by Paypal, bounces to
+The flow is: the first button for Paypal sends a request to Paypal to initialise the transaction and gets a token 
+back in return. If Paypal fails to send back a token, then the module refreshes that page with an error message 
+suggesting that the customer should use your normal payment service provider and shows the cards that you accept. 
+Once the token is read, then your customer is taken to Paypal to login and choose his payment method. Once that is 
+done, he returns to us and hits the 'paypalgetrequest' page. This gets his full address as held by Paypal, bounces to
 the final 'paypalcheckout' page and populates the form with his address details. If you have both shipping
 and billing forms on that page, the shipping address will be populated by default but you may force
 the billing form to be populated instead by sending
 <input type=hidden name=pp_use_billing_address value=1>
-at the initial 'setrequest' stage. Then the customer clicks the final 'pay now' button and the
-transaction is done.
+at the initial stage. Then the customer clicks the final 'pay now' button and the transaction is done.
 
 
 Options that may be set either in the route or in the page:
@@ -219,6 +189,16 @@ Testing: while the obvious test choice is to use their sandbox, I've always foun
    business accounts at minimal cost to yourself, but with the confidence of knowing that test results are correct.
 
 =head1 Changelog
+version 1.0.5, June 2009
+	- fixed bug with Canadian provinces: PP were sending shortened versions of 2 province names, and also 
+	  sometimes sending the 2 letter code (possibly from older a/cs) rather than the full name. Thanks to 
+	  Steve Graham for finding this.
+version 1.0.4, May 2009
+	- re-wrote documentation, including revised and simplified method of co-operating with other payment
+	  systems in log_transaction. 
+
+version 1.0.3, 1.02.2009
+	- fixed bug in handling of thousands separator
 
 version 1.0.2, 22.01.2009 
 	- conversion of Canadian province names to 2 letter variant is now the default
@@ -250,8 +230,8 @@ BEGIN {
 	eval {
 		package Vend::Payment;
 		require SOAP::Lite or die __PACKAGE__ . " requires SOAP::Lite";
-		# without this next it defaults to Net::SSL which may crash
-		require IO::Socket::SSL or die __PACKAGE__ . " requires IO::Socket::SSL";
+# without this next it defaults to Net::SSL which may crash
+		require IO::Socket::SSL or die __PACAKGE__ . "requires IO::Socket::SSL";
 		require Net::SSLeay;
 	};
 
@@ -261,71 +241,48 @@ BEGIN {
 		die $msg;
 	}
 
-	::logGlobal("%s v1.0.2d payment module loaded",__PACKAGE__)
+	::logGlobal("%s v1.0.5b payment module loaded",__PACKAGE__)
 		unless $Vend::Quiet or ! $Global::VendRoot;
 }
 
 package Vend::Payment;
 
 sub paypalexpress {
- use SOAP::Lite +trace; # debugging only
+ #use SOAP::Lite +trace; # debugging only
     my ($token, $header, $request, $method, $response, $in);
 
-### Check that shipping is not zero unless it's allowed to be, and if so return to the checkout
-my $ppcheckzeroshipping =  $::Values->{pp_check_zero_shipping} || charge_param('pp_check_zero_shipping') || '';
-my $ppcheckreturn = $::Values->{ppcheckreturn} || 'ord/checkout';
-my $checkouturl = $Tag->area({ href => "$ppcheckreturn" });
-my $shipmode = $::Values->{mv_shipmode} || charge_param('default_shipmode') || 'upsg';
-my $freeshipping = delete $::Scratch->{freeshipping} || 'no'; # set to 'yes' in a custom 'free shipping' routine to allow free shipping with shipping weight > 0
-my ($shiperror, $shipweight, $shipfree, $shipcost) = split(/:/, $::Tag->shipping({ mode => $shipmode, label => 1, noformat => 1, format => "%M=%e:%T:%F:%F" }));
-if ($shipcost !~ /\d+/) {$shipcost = 0}
- 
-    $::Scratch->{zeroshipping} = '';
-if ($shipfree =~ /free/i || $freeshipping eq 'yes' || $shipweight == 0) {
-	$::Scratch->{zeroshipping} = '1'; # for use in final shipping page
-			}
-  elsif (($ppcheckzeroshipping == 1 ) and (($shiperror =~ /Not enough information/i) || ($shipcost == 0) || ($shipcost != $shipping))) {
-	my $msg = errmsg("Please check that your shipping cost is correct - thank you.");
-    $Vend::Session->{errors}{Shipping} = $msg;
-$::Tag->tag({ op => 'header', body => <<EOB });
-Status: 302 moved
-Location: $checkouturl
-EOB
-}
-
-# find some base values
 	foreach my $x (@_) {
 		    $in = { 
 		    		pprequest => $x->{'pprequest'},
-		    		username  => $x->{'id'},
-		    		password  => $x->{'password'},
-		    		signature => $x->{'signature'}	
 		    	   }
 	}
 
-my $pprequest = $in->{'pprequest'} || charge_param('pprequest') || 'setrequest'; # 'setrequest', 'getrequest', 'dorequest'. 
-my $username  = $in->{'username'}  || charge_param('id') or die "No username id\n";
-my $password  = $in->{'password'}  || charge_param('password') or die "No password\n";
-my $signature = $in->{'signature'} || charge_param('signature') or die "No signature found\n"; # use this as certificate is broken
-my $host      = $::Values->{'pphost'} || charge_param('host') ||  'api-3t.paypal.com'; #  testing 3-token system is 'api-3t.sandbox.paypal.com'.
-my $ca2state  = $::Values->{'ca_2letter_state'} || charge_param('ca_2letter_state') || '1'; # 0 or no to not convert Canadian state/province to uppercased 2 letter variant.
+my $pprequest   = $in->{'pprequest'} || charge_param('pprequest') || 'setrequest'; # 'setrequest' must be the default for standard Paypal. 
+my $username    = charge_param('id') or die "No username id\n";
+my $password    = charge_param('password') or die "No password\n";
+my $signature   = charge_param('signature') or die "No signature found\n"; # use this as certificate is broken
+my $ppcheckreturn = $::Values->{ppcheckreturn} || 'ord/checkout';
+my $checkouturl = $Tag->area({ href => "$ppcheckreturn" });
 
 # ISO currency code, from the page for a multi-currency site or fall back to config files.
 my $currency = $::Values->{currency_code} || $Vend::Cfg->{Locale}{iso_currency_code} ||
                 charge_param('currency')  || $::Variable->{MV_PAYMENT_CURRENCY} || 'USD';
 
-my $amount =  Vend::Interpolate::total_cost() || $::Values->{amount}; # required
+my $amount =  charge_param('amount') || Vend::Interpolate::total_cost() || $::Values->{amount}; # required
    $amount =~ s/^\D*//g;
    $amount =~ s/\s*//g;
    $amount =~ s/,//g;
 
 # for a SET request
 my $sandbox            = $::Values->{ppsandbox} || charge_param('sandbox') || ''; # 1 or yes to use for testing
+   $sandbox            = "sandbox." if $sandbox;
+my $host               = charge_param('host') ||  'api-3t.paypal.com'; #  testing 3-token system is 'api-3t.sandbox.paypal.com'.
+   $host               = 'api-3t.sandbox.paypal.com' if $sandbox;
 my $invoiceID          = $::Values->{inv_no} || $::Values->{mv_transaction_id} || $::Values->{order_number} || ''; # optional
 my $returnURL          = $::Values->{returnurl} || charge_param('returnurl') or die "No return URL found\n"; # required
 my $cancelURL          = $::Values->{cancelurl} || charge_param('cancelurl') or die "No cancel URL found\n"; # required
 my $maxAmount          = $::Values->{maxamount} || '';  # optional
-   $maxAmount          = &ppcommify($maxamount);
+   $maxAmount          = sprintf '%.2f', $maxamount;
 my $orderDescription   = '';
 my $address            = '';
 my $reqConfirmShipping = $::Values->{reqconfirmshipping} || charge_param('reqconfirmshipping') || ''; # you require that the customer's address must be "confirmed"
@@ -348,17 +305,17 @@ my $city               = $::Values->{city};
 my $state              = $::Values->{state};
 my $zip                = $::Values->{zip};
 my $country            = $::Values->{country};
-   if ($country eq 'UK') {$country = 'GB'}; # plonkers reject UK
+   $country = 'GB' if ($country eq 'UK'); # plonkers reject UK
    
 # for a DO request
 my $itemTotal     = $::Values->{itemtotal} || Vend::Interpolate::subtotal() || '';
-   $itemTotal     = &ppcommify($itemtotal);
+   $itemTotal     = sprintf '%.2f', $itemtotal;
 my $shipTotal     = $::Values->{shiptotal} || Vend::Interpolate::shipping($::Values->{mv_shipmode}) || '';
-   $shipTotal     = &ppcommify($shiptotal);
+   $shipTotal     = sprintf '%.2f', $shiptotal;
 my $taxTotal      = $::Values->{taxtotal} || Vend::Interpolate::salestax() || '';
-   $taxTotal      = &ppcommify($taxtotal);
+   $taxTotal      = sprintf '%.2f', $taxtotal;
 my $handlingTotal = $::Values->{handlingtotal} || Vend::Ship::tag_handling() || '';
-   $handlingTotal = &ppcommify($handlingtotal);
+   $handlingTotal = sprintf '%.2f', $handlingtotal;
 
 my $notifyURL           = $::Values->{notifyurl} || charge_param('notifyurl') || ''; # for IPN
 my $buttonSource        = $::Values->{buttonsource} || charge_param('buttonsource') || ''; # for third party source
@@ -370,11 +327,11 @@ my $quantity            = $::Tag->nitems() || '1';
 
 # if $paymentDetailsItem is set, then need to pass an item amount to keep Paypal happy
 my $itemAmount   = $amount / $quantity;
-   $itemAmount   = &ppcommify($itemAmount);
-   $amount       = &ppcommify($amount);
+   $itemAmount   = sprintf '%.2f', $itemAmount;
+   $amount       = sprintf '%.2f', $amount;
 my $receiverType = $::Values->{receiverType} || charge_param('receivertype') || 'EmailAddress'; # used in MassPay
 my $version      = '2.0';
-
+#::logDebug("PP".__LINE__.": amount=$amount, itemamount=$itemAmount; tax=$taxTotal, ship=$shipTotal, hdl=$handlingTotal");
     $order_id  = gen_order_id($opt);
 
 #-----------------------------------------------------------------------------------------------
@@ -447,17 +404,26 @@ my  $receiverEmail = $::Values->{receiveremail} || ''; # address of refund recip
                             )
                           )
                         );
+                        
+            my @giropay = (
+            			SOAP::Data->name("giropaySuccessURL" => $giropaySuccessURL)->type("xs:string"),
+            			SOAP::Data->name("giropayCancelURL" => $giropayCancelURL)->type("xs:string"),
+            			SOAP::Data->name("BanktxnPendingURL" => $BanktxnPendingURL)->type("xs:string")
+            				);
+            
 
 # Destroy the token here at the start of a new request, rather than after a 'dorequest' has completed,
 # as Paypal use it to reject duplicate payments resulting from clicking the final 'pay' button more
 # than once.
-   delete $::Scratch->{token};
+  
    undef $result{Token};
 
     if (($addressOverride == '1') and ($name)) {
     push @setreq, @setaddress;
      }
 
+	push @setreq, @giropay if $giropayaccepted =~ /1|y/;
+	
 		$request = SOAP::Data->name("SetExpressCheckoutRequest" =>
 				\SOAP::Data->value(
 				 SOAP::Data->name("Version" => $version)->type("xs:string"),
@@ -470,70 +436,28 @@ my  $receiverEmail = $::Values->{receiveremail} || ''; # address of refund recip
 
  	    $method = SOAP::Data->name('SetExpressCheckoutReq')->attr({xmlns=>$xmlns});
 	    $response = $service->call($header, $method => $request);
-	    		%result = %{$response->valueof('//SetExpressCheckoutResponse')};
-				$::Scratch->{token} = $result{Token};
+	    	%result = %{$response->valueof('//SetExpressCheckoutResponse')};
+			$::Scratch->{token} = $result{Token};
  
    if (!$result{Token}) {
-    if ($result{Errors}{ErrorCode}) {
-       $::Session->{errors}{PaypalExpress} = $result{Errors}{LongMessage};
+    if ($result{Ack} eq 'Failure') {
+      foreach my $i (@{$result{Errors}}) {
+        $::Session->{errors}{PaypalExpress} .= "$i->{ShortMessage}, ";
+        		}
+        $::Session->{errors}{PaypalExpress} =~ s/, $//;
              }
     else {
        my $accepted = uc($::Variable->{CREDIT_CARDS_ACCEPTED});
-       $::Session->{errors}{PaypalExpress} = errmsg("Paypal has failed to respond correctly - please use our secure payment system instead. We accept $accepted cards");
+       $::Session->{errors}{PaypalExpress} = errmsg("Paypal is currently unavailable - please use our secure payment system instead. We accept $accepted cards");
              }
-$::Tag->tag({ op => 'header', body => <<EOB });
-Status: 302 moved
-Location: $checkouturl
-EOB
-return();
+return $Tag->deliver({ location => $checkouturl }) 
       }
 
-# Write full order to orders/paypal using pp$date.$session_id file name as failsafe backup in case order 
-# route fails - if it does fail PP will still take the customer's money. IC's routine in Order.pm populates 
-# the basket but not the address at this point: we want both if available
-	
-	my ($item, $itm, $basket);
-	my $date = $Tag->time({ body => "%Y%m%d%H%M" });
-	my $fn = Vend::Util::catfile(
-				charge_param('orders_dir') || 'orders/paypal',
-				"pp$date.$::Session->{id}"  
-			);
-	
-	mkdir "orders/paypal", 0775 unless -d "orders/paypal";
-	
-	foreach  $item (@{$::Carts->{'main'}}) {
-	    $itm = {
-	    		code => $item->{'code'},
-				quantity => $item->{'quantity'},
-				description => Vend::Data::item_description($item),
-				price => Vend::Data::item_price($item),
-				subtotal => Vend::Data::item_subtotal($item)
-				};
-   $basket .= <<EOB;
-Item = $itm->{code}, "$itm->{description}"; Price = $itm->{price}; Qty = $itm->{quantity}; Subtotal = $itm->{subtotal} 
-EOB
- }
-   $basket .= <<EOB;
-(Neither tax nor shipping had been calculated when this record was made)
-Delivery address:
-$::CGI->{fname} $::CGI->{lname}
-$::CGI->{address1}  $::CGI->{address2}
-$::CGI->{city}
-$::CGI->{state} $::CGI->{zip}
-$::CGI->{country}
-EOB
-
-   Vend::Util::writefile( $fn, $basket )
-				or ::logError("Paypal error writing failsafe order $fn: $!");
-
+#::logDebug("PP".__LINE__.": sandbox=$sandbox; host=$host");
 # Now go off to Paypal
-  my $sbx = 'sandbox.' if $sandbox;
-  my $redirecturl = "https://www."."$sbx"."paypal.com/cgi-bin/webscr?cmd=_express-checkout&token=$result{Token}";
+  my $redirecturl = "https://www."."$sandbox"."paypal.com/cgi-bin/webscr?cmd=_express-checkout&token=$result{Token}";
 
-$::Tag->tag({ op => 'header', body => <<EOB });
-Status: 302 moved
-Location: $redirecturl
-EOB
+return $Tag->deliver({ location => $redirecturl }) 
 
    }
 
@@ -550,6 +474,7 @@ EOB
 	     $method = SOAP::Data->name('GetExpressCheckoutDetailsReq')->attr({xmlns=>$xmlns});
 	    $response = $service->call($header, $method => $request);
 			%result = %{$response->valueof('//GetExpressCheckoutDetailsResponse')};
+#::logDebug("PP".__LINE__.": Get Ack=$result{Ack}");
 
 # populate the billing address rather than shipping address when the basket is being shipped to
 # another address, eg it is a wish list.
@@ -594,10 +519,12 @@ EOB
 	    $::Values->{city}           = $result{GetExpressCheckoutDetailsResponseDetails}{PayerInfo}{Address}{CityName};
 	    $::Values->{state}          = $result{GetExpressCheckoutDetailsResponseDetails}{PayerInfo}{Address}{StateOrProvince};
 	    $::Values->{zip}            = $result{GetExpressCheckoutDetailsResponseDetails}{PayerInfo}{Address}{PostalCode};
-	    $::Values->{country}        = $result{GetExpressCheckoutDetailsResponseDetails}{PayerInfo}{Address}{Country};
 	    $::Values->{countryname}    = $result{GetExpressCheckoutDetailsResponseDetails}{PayerInfo}{Address}{CountryName};
+		$::Values->{country}        = $result{GetExpressCheckoutDetailsResponseDetails}{PayerInfo}{Address}{Country};
 	              }
 		   }
+		   
+		$::Values->{company} = $::Values->{b_company} = $::Values->{payerbusiness};
 
 # If shipping address and name are chosen at Paypal to be different to the billing address/name, then {name} contains 		
 # the shipping name but {fname} and {lname} still contain the billing names.
@@ -611,20 +538,32 @@ EOB
    
        $country = $::Values->{country} || $::Values->{b_country};
        $state = $::Values->{state} || $::Values->{b_state};
-  if (($country eq 'CA') and ($ca2state =~ /1|y/)) {
-    my $db  = dbref('state') or warn errmsg("PP cannot open state table");
-    my $dbh = $db->dbh() or warn errmsg("PP cannot get handle for tbl 'state'");
-    my $sth = $dbh->prepare("SELECT state FROM state WHERE name='$state' AND country='CA'");
-       $sth->execute() or warn errmsg("PP cannot execute at ln610");
-       $state = $sth->fetchrow() or warn errmsg("PP no state unless defined $state");
-       
-       $::Values->{b_state} = $state if ($::Values->{pp_use_billing_address} == 1);
-       $::Values->{state} = $state;
-      }
-   }
+
+# Remap Canadian provinces rather than lookup the db, as some Paypal names are incomplete wrt the official names. 
+# It seems that some PP accounts, possibly older ones, send the 2 letter abbreviation rather than the full name.
+	if ($country eq 'CA') {		
+		$state = 'AB' if ($state =~ /Alberta|^AB$/i);
+		$state = 'BC' if ($state =~ /British Columbia|^BC$/i);
+		$state = 'MB' if ($state =~ /Manitoba|^MB$/i);
+		$state = 'NB' if ($state =~ /New Brunswick|^NB$/i);
+		$state = 'NL' if ($state =~ /Newfoundland|^NL$/i);
+		$state = 'NS' if ($state =~ /Nova Scotia|^NS$/i);
+		$state = 'NT' if ($state =~ /Northwest Terr|^NT$/i);
+		$state = 'NU' if ($state =~ /Nunavut|^NU/i);
+		$state = 'ON' if ($state =~ /Ontario|^ON$/i);
+		$state = 'PE' if ($state =~ /Prince Edward|^PE$/i);
+		$state = 'QC' if ($state =~ /Quebec|^QC$/i);
+		$state = 'SK' if ($state =~ /Saskatchewan|^SK$/i);
+		$state = 'YT' if ($state =~ /Yukon|^YT$/i);
+	}
+        
+        $::Values->{b_state} = $state if ($::Values->{pp_use_billing_address} == 1);
+        $::Values->{state} = $state;
+  
+  }
 
 #------------------------------------------------------------------------------------------------
-### Create a DO request and method, and read response
+### Create a DO request and method, and read response. Not used for Giropay
  elsif ($pprequest eq 'dorequest') {
      #  $currency = 'EUR'; # set to currency different to that started with to force failure for testing
 	   my @doreq  = (
@@ -663,11 +602,11 @@ EOB
 	                    )
 	                  )->type("ebl:PaymentDetailsItemType")
 	                );
-
-	if ($notifyURL) {push @doreq,SOAP::Data->name("NotifyURL" => $notifyURL )->type("xs:string")}
-	if ($buttonSource) {push @doreq, SOAP::Data->name("ButtonSource" => $buttonSource )->type("xs:string")}
-	if ($addressOverride  == '1') {push @doreq, @sta }
-	if ($paymentDetailsItem == '1') {push @doreq, @pdi }
+            
+	push @doreq, SOAP::Data->name("NotifyURL" => $notifyURL )->type("xs:string") if $notifyURL;
+	push @doreq, SOAP::Data->name("ButtonSource" => $buttonSource )->type("xs:string") if $buttonSource;
+	push @doreq, @sta if $addressOverride  == '1';
+	push @doreq, @pdi if $paymentDetailsItem == '1';
 
 	    $request = SOAP::Data->name("DoExpressCheckoutPaymentRequest" =>
 			       \SOAP::Data->value(
@@ -689,7 +628,8 @@ EOB
 	    $method = SOAP::Data->name('DoExpressCheckoutPaymentReq')->attr({xmlns=>$xmlns});
 	    $response = $service->call($header, $method => $request);
 	    %result = %{$response->valueof('//DoExpressCheckoutPaymentResponse')};
-
+#::logDebug("PP".__LINE__.": Do Ack=$result{Ack}; ppreq=$pprequest");
+	  
 	  if ($result{Ack} eq "Success") {
 	    $Session->{payment_result}{Status} = 'Success';
 	    $result{TransactionID}       = $result{DoExpressCheckoutPaymentResponseDetails}{PaymentInfo}{TransactionID};
@@ -703,6 +643,7 @@ EOB
 	    $result{ReasonCode}          = $result{DoExpressCheckoutPaymentResponseDetails}{PaymentInfo}{ReasonCode};
 	    $result{FeeAmount}           = $result{DoExpressCheckoutPaymentResponseDetails}{PaymentInfo}{FeeAmount};
 	    $result{ExchangeRate}        = $result{DoExpressCheckoutPaymentResponseDetails}{PaymentInfo}{ExchangeRate};
+		$result{giropaytrue}         = $result{DoExpressCheckoutPaymentResponseDetails}{RedirectRequired};
 
 			    }
  	  else  {
@@ -711,12 +652,87 @@ EOB
 	    $result{LongMessage}  = $result{Errors}{LongMessage};
 		$::Session->{errors}{"PaypalExpress error"}  = $result{Errors}{LongMessage};
 		    }
+#::logDebug("PP".__LINE__.": errors=$result{ShortMessage}");
+		
+	}
+
+
+#-------------------------------------------------------------------------------------------------
+# REFUND transaction
+ elsif ($pprequest eq 'refund') {
+
+	   my @refreq = (
+                    SOAP::Data->name("Version" => $version)->type("xs:string")->attr({xmlns=>"urn:ebay:apis:eBLBaseComponents"}),
+                    SOAP::Data->name("TransactionID" => $transactionID)->type("ebl:TransactionId"),
+                    SOAP::Data->name("RefundType" => $refundType)->type(""),
+                    SOAP::Data->name("Memo" => $memo)->type("xs:string")
+                     );
+
+   if ($refundType eq 'Partial') {
+    push @refreq,  SOAP::Data->name("Amount" => $amount)->attr({"currencyID"=>$currency})->type("cc:BasicAmountType")
+                  }
+
+     $request = SOAP::Data->name("RefundTransactionRequest" =>
+                \SOAP::Data->value( @refreq
+                    )
+                  ) ->type("ns:RefundTransactionRequestType");
+
+	    $method = SOAP::Data->name('RefundTransactionReq')->attr({xmlns=>$xmlns});
+	    $response = $service->call($header, $method => $request);
+	    %result = %{$response->valueof('//RefundTransactionResponse')};
+	    	
+	    	if ($result{Ack} eq "Success") {
+	  		$::Session->{payment_result}{Terminal} = 'success';
+	      	$::Session->{payment_result}{RefundTransactionID} = $result{RefundTransactionResponse}{RefundTransctionID};
+	    		}
 		}
 
 #-------------------------------------------------------------------------------------------------
+# MASSPAY transaction
+ elsif ($pprequest eq 'masspay') {
+       # TODO: handle multiple entries
+    my $receiverlist = $::Values->{receiverlist};
+		if ($receiverType eq 'EmailAddress') {
+		$receiverType = SOAP::Data->name("ReceiverEmail" => $::Values->{email})->type("ebl:EmailAddressType");
+			}
+		else {
+		$receiverType = SOAP::Data->name("ReceiverID" => $::Values->{payerid})->type("xs:string");
+		}
+
+	foreach my $rx (split /\n/, $receiverlist) {
+		my @mpi = (
+                  SOAP::Data->name("MassPayItem" =>
+                   \SOAP::Data->value(
+                    $receiverType,
+                    SOAP::Data->name("Amount" => $amount)->attr({ "currencyID"=>$currency })->type("ebl:BasicAmountType"),
+                    SOAP::Data->name("UniqueID" => $orderid)->type("xs:string"),
+                    SOAP::Data->name("Note" => $memo)->type("xs:string")
+                    )
+                 ) ->type("ns:MassPayItemRequestType")
+              );
+
+	$request = SOAP::Data->name("MassPayRequest" =>
+			   \SOAP::Data->value(
+                SOAP::Data->name("Version" => $version)->type("xs:string")->attr({ xmlns=>"urn:ebay:apis:eBLBaseComponents" }),
+			    SOAP::Data->name("EmailSubject" => $emailSubject)->type("xs:string"),
+			    SOAP::Data->name("ReceiverType" => $receiverType)->type(""),
+                @mpi
+                   )
+                 ) ->type("ns:MassPayRequestType");
+
+	    $method = SOAP::Data->name('MassPayReq')->attr({ xmlns=>$xmlns });
+	    $response = $service->call($header, $method => $request);
+	    %result = %{$response->valueof('//MassPayResponse')};
+	  	$::Session->{payment_result}{Terminal} = 'success' if $result{Ack} eq 'Success';
+#::logDebug("PP".__LINE__.":response=$result{Ack},cID=$result{CorrelationID}");
+			}
+	 	$::Values->{receiverlist} = '';
+      }
+
+#-------------------------------------------------------------------------------------------------
     # Interchange names are on the left, Paypal on the right
  my %result_map;
- if ($pprequest eq 'dorequest') {
+ if ($pprequest =~ /dorequest|giropaylog/) {
     %result_map = ( qw/
 		   order-id	     		TransactionID
 		   pop.order-id			TransactionID
@@ -725,7 +741,6 @@ EOB
 		   pop.status			Ack
 		   pop.txn-id			TransactionID
 		   pop.refund-txn-id	RefundTransactionID
-		   pop.statusdetail		Errors.LongMessage
 		   pop.cln-id			CorrelationID
 	/
     );
@@ -735,10 +750,11 @@ EOB
            if defined $result{$result_map{$_}};
     }
   }
-
-  if (($result{Ack} eq 'Success') and ($pprequest eq 'dorequest')) {
+#::logDebug("PP".__LINE__.": ack=$result{Ack}; ppreq=$pprequest");
+  if (($result{Ack} eq 'Success') and ($pprequest =~ /dorequest|giropay/)) {
          $result{MStatus} = $result{'pop.status'} = 'success';
          $result{'order-id'} ||= $opt->{order_id};
+#::logDebug("PP".__LINE__.": mstatus=$result{MStatus}"); 
            }
   elsif (!$result{Ack}) {
          $result{MStatus} = $result{'pop.status'} = 'success';
@@ -754,18 +770,11 @@ EOB
 
 delete $::Values->{returnurl};
 
-
+#::logDebug("PP".__LINE__." result:" .::uneval(\%result));
     return (%result);
 
 }
 
-sub ppcommify {
-    local($_) = shift;
-    $_ = sprintf '%.2f', $_;
-    1 while s/^(-?\d+)(\d{3})/$1,$2/;
-    return $_;
-}
-
 package Vend::Payment::PaypalExpress;
 
 1;
diff --git a/lib/Vend/Payment/SagePay.pm b/lib/Vend/Payment/SagePay.pm
new file mode 100644
index 0000000..d3645fc
--- /dev/null
+++ b/lib/Vend/Payment/SagePay.pm
@@ -0,0 +1,1245 @@
+# Vend::Payment::SagePay - Interchange Sagepay support
+#
+# SagePay.pm, v 0.8.7, May 2009
+#
+# Copyright (C) 2009 Zolotek Resources Ltd. All rights reserved.
+#
+# Author: Lyn St George <info at zolotek.net, http://www.zolotek.net>
+# Based on original code by Mike Heins <mheins at perusion.com> and others.
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public Licence as published by
+# the Free Software Foundation; either version 2 of the Licence, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public Licence for more details.
+#
+# You should have received a copy of the GNU General Public
+# Licence along with this program; if not, write to the Free
+# Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
+# MA  02111-1307  USA.
+
+
+
+package Vend::Payment::SagePay;
+
+=head1 Interchange Sagepay Support
+
+Vend::Payment::SagePay $Revision: 0.8.7 $
+
+http://kiwi.zolotek.net is the home page with the latest version.
+
+=head1 This package is for the 'SagePay Direct' payment system.
+
+Note that their 'Direct' system is the only one which leaves the customer on
+your own site and takes payment in real time. Their other systems, eg Terminal
+or Server, do not require this module.
+
+
+
+=head1 Quick Start Summary
+
+1 Place this module in <IC_root>/lib/Vend/Payment/SagePay.pm
+
+2 Call it in interchange.cfg with:
+    Require module Vend::Payment::SagePay
+
+3 Add into variable.txt (tab separated):
+    MV_PAYMENT_MODE   sagepay
+  Add a new route into catalog.cfg (options for the last entry in parentheses):
+    Route sagepay id YourSagePayID
+    Route sagepay host live.sagepay.com (test.sagepay.com)
+    Route sagepay currency GBP (USD, EUR, others, defaults to GBP)
+    Route sagepay txtype PAYMENT (AUTHENTICATE, DEFERRED)
+    Route sagepay available yes (no, empty)
+    Route sagepay logzero yes (no, empty)
+    Route sagepay logorder yes (no, empty)
+    Route sagepay logsagepay yes (no, empty)
+    Route sagepay applyavscv2 '0': if enabled then check, and if rules apply use.
+                    '1': force checks even if not enabled; if rules apply use.
+                    '2': force NO checks even if enabled on account.
+                    '3': force checks even if not enabled; do NOT apply rules.
+    Route sagepay giftaidpayment 0 (1 to donate tax to Gift Aid)
+
+4 Create a new locale setting for en_GB as noted in "item currency" below, and copy the
+public space interchange/en_US/ directory to a new interchange/en_GB/ one. Ensure that any
+other locales you might use have a correctly named directory as well. Ensure that this locale
+is found in your version of locale.txt (and set up GB as opposed to US language strings to taste).
+
+5 Create entry boxes on your checkout page for: 'mv_credit_card_issue_number', 'mv_credit_card_start_month',
+'mv_credit_card_start_year', 'mv_credit_card_type' and  'mv_credit_card_cvv2'.
+
+6 The new fields in API 2.23 are: BillingAddress, BillingPostCode, DeliveryAddress, DeliveryPostCode,
+BillingFirstnames, BillingSurname, DeliveryFirstnames, DeliverySurname, ContactNumber,ContactFax,CustomerEmail.
+CustomerName has been removed. Billing and Delivery State must be sent if the destination country is the US, otherwise
+they are not required. State must be only 2 letters if sent. Other fields may default to a space if there
+is no proper value to send, though this may conflict with your AVS checking rules. SagePay currently 
+accept a space as of time of writing - if they change this without changing the API version then send
+either a series of '0' or '-' characters to stop their error messages. 
+
+7. Add a page in pages/ord/, tdsfinal.html, being a minimal page with only the header and side bar,
+and in the middle of the page put:
+[if scratch acsurl]
+	  <tr>
+		<td align=center height=600 valign=middle colspan=2>
+		  <iframe src="__CGI_URL__/ord/tdsauth.html" frameborder=0 width=600 height=600></iframe>
+		</td>
+	  </tr>
+[/if]
+
+Add a page in pages/ord/, tdsauth.html, consisting of this:
+<body onload="document.form.submit();">
+<FORM name="form" action="[scratchd acsurl]" method="POST" />
+<input type="hidden" name="PaReq" value="[scratch pareq]" />
+<input type="hidden" name="TermUrl" value="[scratch termurl]" />
+<input type="hidden" name="MD" value="[scratch md]" />
+</form>
+</body>
+along with whatever <noscript> equivalent you want. This will retrieve the bank's page within the iframe.
+
+Add a page in pages/ord/, tdsreturn.html, consisting of this:
+	[charge route="sagepay" sagepayrequest="3dsreturn"]
+	<p>
+	   <blockquote>
+        <font color="__CONTRAST__">
+                [error all=1 keep=1 show_error=1 show_label=1 joiner="<br>"]
+        </font>
+       </blockquote>
+
+The iframe in 'tdsfinal' will be populated with the contents of 'tdsauth', and the javascript will
+automatically display the bank's authentication page. When the customer clicks 'Submit' at the bank's
+page, the iframe contents will be replaced with the 'tdsreturn' page, which will complete the route 
+and display the receipt inside the iframe. If the customer clicks 'cancel' at the bank, then this 
+'tdsreturn' page will stay put and display whatever message you have put there along with the error message. 
+The value of [scratch tds] is set true for a 3DSecure transaction only, so can be used for messages
+etc on the receipt page. 
+
+8. When running a card through 3DSecure, the route is run twice: firstly to Sagepay who check whether or
+not the card is part of 3DSecure - if it is they send the customer to the bank's authentication page
+and upon returning from that the route must be run a second time to send the authentication results to
+Sagepay. The second run is initiated from the 'ord/tdsreturn' page, not from etc/log_transaction as it normally
+would be. To handle this change to the normal system flow you need to alter log_transaction to make the 
+call to the payment module conditional,ie, wrap the following code around the "[charge route...]" call 
+found in ln 172 (or nearby):
+	[if scratchd mstatus eq success]
+	[tmp name="charge_succeed"][scratch order_id][/tmp]
+	[else]
+	[tmp name="charge_succeed"][charge route="[var MV_PAYMENT_MODE]" amount="[scratch tmp_remaining]" order_id="[value mv_transaction_id]"][/tmp]
+	[/else]
+	[/if]
+If the first call to Sagepay returns a request to send the customer to the 3DSecure server, then IC will 
+write a payment route error to the error log prior to sending the customer there. This error stops the
+route completing and lets the 3DSecure process proceed as it should. This error is not raised if the card
+is not part of 3DSecure, and instead the route completes as it normally would. 
+
+Also add this line just after '&final = yes' near the end of the credit_card section of etc/profiles.order:
+	&set=mv_payment_route sagepay
+
+9. Add these new fields into log_transaction, to record the values returned from Sagepay (these will be
+key in identifying transactions and problems in any dispute with them):
+
+mv_credit_card_type: [calc]$Session->{payment_result}{CardType}[/calc]
+mv_credit_card_issue_number: [value mv_credit_card_issue_number]
+txtype:  [calc]$Session->{payment_result}{TxType};[/calc]
+vpstxid: [calc]$Session->{payment_result}{VPSTxID};[/calc]
+txauthno: [calc]$Session->{payment_result}{TxAuthNo};[/calc]
+securitykey: [calc]$Session->{payment_result}{SecurityKey};[/calc]
+vendortxcode:  [calc]$Session->{payment_result}{VendorTxCode};[/calc]
+avscv2: [calc]$Session->{payment_result}{AVSCV2};[/calc]
+addressresult:[calc]$Session->{payment_result}{AddressResult};[/calc]
+postcoderesult: [calc]$Session->{payment_result}{PostCodeResult};[/calc]
+cv2result: [calc]$Session->{payment_result}{CV2Result};[/calc]
+securestatus:[calc]$Session->{payment_result}{SecureStatus};[/calc]
+pares: [calc]$Session->{payment_result}{PaRes};[/calc]
+md: [calc]$Session->{payment_result}{MD};[/calc]
+cavv: [calc]$Session->{payment_result}{CAVV};[/calc]
+and add these into your MySQL or Postgres transactions table, as type varchar(128) except for 'pares'
+which should be type 'text'.
+
+Note that there is no 'TxAuthNo' returned for a successful AUTHENTICATE.
+
+=head1 PREREQUISITES
+
+  Net::SSLeay
+    or
+  LWP::UserAgent and Crypt::SSLeay
+
+  wget - a recent version built with SSL and supporting the 'connect' timeout function.
+
+
+=head1 DESCRIPTION
+
+The Vend::Payment::SagePay module implements the SagePay() routine for use with
+Interchange. It is _not_ compatible on a call level with the other Interchange
+payment modules - SagePay does things rather differently. 
+
+To enable this module, place this directive in C<interchange.cfg>:
+
+    Require module Vend::Payment::SagePay
+
+This I<must> be in interchange.cfg or a file included from it.
+
+Make sure CreditCardAuto is off (default in Interchange demos).
+
+
+=head1 The active settings.
+
+The module uses several of the standard settings from the Interchange payment routes.
+Any such setting, as a general rule, is obtained first from the tag/call options on
+a page, then from an Interchange order Route named for the mode in catalog.cfg,
+then a default global payment variable in products/variable.txt, and finally in
+some cases a default will be hard-coded into the module.
+
+=item Mode
+
+The mode can be named anything, but the C<gateway> parameter must be set
+to C<sagepay>. To make it the default payment gateway for all credit card
+transactions in a specific catalog, you can set in C<catalog.cfg>:
+
+    Variable   MV_PAYMENT_MODE  sagepay
+or in variable.txt:
+    MV_PAYMENT_MODE sagepay (tab separated)
+
+if you want this to cooperate with other payment systems, eg PaypalExpress, then see the documentation
+that comes with that system - it should be fully explained there.
+
+
+=item id
+
+Your SagePay vendor ID, supplied by SagePay when you sign up. Various ways to state this:
+in variable.txt:
+    MV_PAYMENT_ID   YourSagePayID Payment
+or in catalog.cfg either of:
+    Route sagepay id YourSagePayID
+    Variable MV_PAYMENT_ID      YourSagePayID
+or on the page
+    [charge route=sagepay id=YourSagePayID]
+
+
+=item txtype
+
+The transaction type is one of: PAYMENT, AUTHENTICATE, DEFERRED for an initial purchase
+through the catalogue, and then can be one of: AUTHORISE, REFUND, RELEASE, VOID, ABORT for payment
+operations through the virtual terminal.
+
+The transaction type is taken firstly from a dynamic variable in the page, meant
+primarily for use with the 'virtual payment terminal', viz: 'transtype' in a select box
+though this could usefully be taken from a possible entry in the products database
+if you have different products to be sold on different terms; then falling back to
+a 'Route txtype PAYMENT' entry in catalog.cfg; then falling back to a global
+variable in variable.txt, eg 'MV_PAYMENT_TXTYPE PAYMENT Payment'; and finally
+defaulting to 'PAYMENT' hard-coded into the module. This variable is returned to
+the module and logged using the value returned from SagePay, rather than a value from
+the page which possibly may not exist.
+
+
+=item available
+
+If 'yes', then the module will check that the gateway is responding before sending the transaction.
+If it fails to respond within 9 seconds, then the module will go 'off line' and log the transaction
+as though this module had not been called. It will also log the txtype as 'OFFLINE' so that you
+know you have to put the transaction through manually later (you will need to capture the card
+number to do this). The point of this is that your customer has the transaction done and dusted,
+rather than being told to 'try again later' and leaving for ever. If not explicitly 'yes',
+defaults to 'no'. NB: if you set this to 'yes', then add into the etc/report that is sent to you:
+Txtype = [calc]$Session->{payment_result}{TxType};[/calc]. Note that you need to have
+a recent version of wget which supports '--connect-timeout' to run this check. Note also that,
+as this transaction has not been logged anywhere on the SagePay server, you cannot use their
+terminal to process it. You must use a virtual terminal which includes a function for this purpose,
+and updates the existing order number with the new payment information returned from SagePay. Note
+further that if you have SagePay set up to require the CV2 value, then virtual terminal should disable
+CV2 checking at run-time by default for such a transaction (logging the CV2 value breaks Visa/MC
+rules and so it can't be legally available for this process).
+
+
+=item logzero
+
+If 'yes', then the module will log a transaction even if the amount sent is zero (which the
+gateway would normally reject). The point of this is to allow a zero amount in the middle of a
+subscription billing series for audit purposes. If not explicitly 'yes', defaults to 'no'.
+Note: this is only useful if you are using an invoicing system or the Payment Terminal, both of which
+by-pass the normal IC processes. IC will allow an item to be processed at zero total price but simply
+bypasses the gateway when doing so.
+
+
+=item logempty
+If 'yes, then if the response from SagePay is read as empty (ie, zero bytes) then the module will use the
+VendorTxID to check on the Sagepay txstatus page to see if that transaction has been logged. If it has then
+the result found on that page will be used to push the result to either success or failure and log accordingly.
+There are two markers set to warn of this:
+$Session->{payment_result}{TxType} will be NULL,
+$Session->{payment_result}{StatusDetail} will be: 'UNKNOWN status - check with SagePay before dispatching goods'
+and you should include these into the report emailed to you. It will also call a logorder Usertag to log
+a backup of the order: if you don't already have this then get it from ftp.zolotek.net/mv/logorder.tag
+
+If the result is not found on that txstatus page then the result is forced to 'failure' and the transaction 
+shown as failed to the customer. 
+
+
+=item card_type
+
+SagePay requires that the card type be sent. Valid types are: VISA, MC, AMEX, DELTA, SOLO, MAESTRO, UKE,
+JCB, DINERS (UKE is Visa Electron issued in the UK).
+
+You may display a select box on the checkout page like so:
+
+              <select name=mv_credit_card_type>
+          [loop
+                  option=mv_credit_card_type
+                  acclist=1
+                  list=|
+VISA=Visa,
+MC=MasterCard,
+SOLO=Solo,
+DELTA=Delta,
+MAESTRO=Maestro,
+AMEX=Amex,
+UKE=Electron,
+JCB=JCB,
+DINERS=Diners|]
+          <option value="[loop-code]"> [loop-param label]
+          [/loop]
+          </select>
+
+
+=item currency
+
+SagePay requires that a currency code be sent, using the 3 letter ISO currency code standard,
+eg, GBP, EUR, USD. The value is taken firstly from either a page setting or a
+possible value in the products database, viz 'iso_currency_code'; then falling back
+to the locale setting - for this you need to add to locale.txt:
+
+    code    en_GB   en_EUR  en_US
+    iso_currency_code   GBP EUR USD
+
+It then falls back to a 'Route sagepay currency EUR' type entry in catalog.cfg;
+then falls back to a global variable (eg MV_PAYMENT_CURRENCY EUR Payment); and
+finally defaults to GBP hard-coded into the module. This variable is returned to
+the module and logged using the value returned from SagePay, rather than a value from
+the page which possibly may not exist.
+
+
+=item cvv2
+
+This is sent to SagePay as mv_credit_card_cvv2. Put this on the checkout page:
+
+    <b>CVV2: &nbsp; <input type=text name=mv_credit_card_cvv2 size=6></b>
+
+but note that under Card rules you must not log this value anywhere.
+
+
+=item issue_number
+
+This is used for some debit cards, and taken from an input box on the checkout page:
+
+    Card issue number: <input type=text name=mv_credit_card_issue_number value='' size=6>
+
+=item mvccStartDate
+
+This is used for some debit cards, and is taken from select boxes on the
+checkout page in a similar style to those for the card expiry date. The labels to be
+used are: 'mv_credit_card_start_month', 'mv_credit_card_start_year'. Eg:
+
+		  <select name=mv_credit_card_start_year>
+		  [loop option=start_date_year lr=1 list=`
+		  my $year = $Tag->time( '', { format => '%Y' }, '%Y' );
+		  my $out = '';
+		  for ($year - 7 .. $year) {
+				  /\d\d(\d\d)/;
+				  $last_two = $1;
+				  $out .= "$last_two\t$_\n";
+		  }
+		  return $out;
+		  `]
+		  <option value="[loop-code]"> [loop-pos 1]
+		  [/loop]
+		  </select>
+
+Make the select box for the start month a copy of the existing one for the expiry month, but with
+the label changed and the addition of 
+= --select --, 
+as the first entry. This intentionally returns nothing for that selection and prevents the StartDate being sent.
+
+
+=item SagePay API v2.23 extra functions
+ApplyAVSCV2 set to:
+	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.
+	You may pass this value from the page as 'applyavscv2' to override the payment route setting.
+	They also have Paypal integrated into this version, but I have no interest in implementing Paypal 
+	through Sagepay. There is a separate PaypalExpress module for that. 
+
+ContactFax: optional
+GiftAidPayment: set to -
+	0 = This transaction is not a Gift Aid charitable donation(default)
+	1 = This payment is a Gift Aid charitable donation and the customer has AGREED to donate the tax.
+	You may pass this value from the page as 'giftaidpayment'
+	
+ClientIPAddress: will show in SagePay reports, and they will attempt to Geo-locate the IP.
+
+
+=item AVSCV2
+SagePay do not use your rulebase or return any checks for these when using 3ds and AUTHORISE. As this data
+is essential for many business models you should use DEFERRED instead. While thought was given to
+running a PAYMENT and VOID for £1 first, simply to get the AVSCV2 results, this cannot be done
+with Maestro cards which legally must go through 3ds and so I have abandoned the idea. 
+
+
+=item Encrypted email with card info
+
+If you want to add the extra fields (issue no, start date) to the PGP message
+emailed back to you, then set the following in catalog.cfg:
+
+Variable<tab>MV_CREDIT_CARD_INFO_TEMPLATE Card type: {MV_CREDIT_CARD_TYPE}; Card no: {MV_CREDIT_CARD_NUMBER}; Expiry: {MV_CREDIT_CARD_EXP_MONTH}/{MV_CREDIT_CARD_EXP_YEAR}; Issue no: {MV_CREDIT_CARD_ISSUE_NUMBER}; StartDate: {MV_CREDIT_CARD_START_MONTH}/{MV_CREDIT_CARD_START_YEAR}
+
+
+=item testing
+
+The SagePay test site is test.sagepay.com, and their live site is
+live.sagepay.com. Enable one of these in MV_PAYMENT_HOST in variable.txt
+(*without* any leading https://) or as 'Route sagepay host test.sagepay.com' in
+catalog.cfg. Be aware that the test site is not an exact replica of the live site, and errors there
+can be misleading. In particular the "SecureStatus" returned from the test site is
+liable to be 'NOTAUTHED' when the live site will return 'OK'.
+
+
+=item methods
+
+An AUTHENTICATE will check that the card is not stolen and contains sufficient funds.
+SagePay will keep the details, so that you may settle against this a month or more
+later. Against an AUTHENTICATE you may do an AUTHORISE (which settles the transaction).
+
+A DEFERRED will place a shadow ('block') on the funds for seven days (or so, depending
+on the acquiring bank). Against a DEFERRED you may do a RELEASE to settle the transaction.
+
+A PAYMENT will take the funds immediately. Against a PAYMENT, you may do a
+REFUND or REPEAT.
+
+A REPEAT may be performed against an AUTHORISE or a PAYMENT. This will re-check and
+take the funds in real time. You may then REPEAT a REPEAT, eg for regular
+subscriptions. As you need to send the amount and currency with each REPEAT, you
+may vary the amount of the REPEAT to suit a variation in subscription fees.
+
+A RELEASE is performed to settle a DEFERRED. Payment of the originally specified
+amount is guaranteed if the RELEASE is performed within the seven days for which
+the card-holder's funds are 'blocked'.
+
+A REFUND may be performed against a PAYMENT, RELEASE, or REPEAT. It may be for a
+partial amount or the entire amount, and may be repeated with several partial
+REFUNDs so long as the total does not exceed the original amount.
+
+A DIRECTREFUND sends funds from your registered bank account to the nominated credit card.
+This does not need to refer to any previous transaction codes, and is useful if you need to
+make a refund but the customer's card has changed or the original purchase was not made by card.
+
+
+=head2 Troubleshooting
+
+Try a sale with  any other test number given by SagePay, eg:
+	Visa VISA 4929 0000 0000 6
+    Mastercard  MC 5404 0000 0000 0001
+    Delta DELTA 4462 0000 0000 0000 0003
+    Visa Electron UK Debit  	UKE  	4917300000000008
+    Solo SOLO 6334 9000 0000 0000 0005 issue no 1
+    Switch (UK Maestro) MAESTRO 5641 8200 0000 0005 issue no 01.
+    Maestro MAESTRO 300000000000000004
+	AmericanExpress AMEX  	3742 0000 0000 004
+
+
+You need these following values to ensure a positive response:
+CV2: 123
+Billing Address: 88
+Billing PostCode: 412
+and the password at their test server is 'password'.
+
+
+If nothing works:
+
+=over 4
+
+=item *
+
+Make sure you "Require"d the module in interchange.cfg:
+
+    Require module Vend::Payment::SagePay
+
+=item *
+
+Make sure either Net::SSLeay or Crypt::SSLeay and LWP::UserAgent are installed
+and working. You can test to see whether your Perl thinks they are:
+
+    perl -MNet::SSLeay -e 'print "It works\n"'
+or
+    perl -MLWP::UserAgent -MCrypt::SSLeay -e 'print "It works\n"'
+
+If either one prints "It works." and returns to the prompt you should be OK
+(presuming they are in working order otherwise).
+
+=item *
+
+Check the error logs, both catalogue and global. Make sure you set your payment
+parameters properly. Try an order, then put this code in a page:
+
+    <XMP>
+    [calc]
+        my $string = $Tag->uneval( { ref => $Session->{payment_result} });
+        $string =~ s/{/{\n/;
+        $string =~ s/,/,\n/g;
+        return $string;
+    [/calc]
+    </XMP>
+
+That should show what happened.
+
+=item *
+
+If you have a PGP/GPG failure when placing an order through your catalogue
+then this may cause the module to be immediately re-run. As the first run would
+have been successful, meaning that both the basket and the credit card information
+would have been emptied, the second run will fail. The likely error message within
+the catalogue will be:
+"Can't figure out credit card expiration". Fixing PGP/GPG will fix this error.
+
+If you get the same error message within the Virtual Terminal, then you haven't
+set the order route as noted above.
+
+
+=item *
+
+If all else fails, Zolotek and other consultants are available to help
+with integration for a fee.
+
+=back
+
+
+=head1 AUTHORS
+
+Lyn St George <info at zolotek.net>, based on original code by Mike Heins
+<mike at perusion.com> and others.
+
+=head2 CREDITS
+Hillary Corney (designersilversmiths.co.uk), Jamie Neil (versado.net),
+Andy Mayer (andymayer.net) for testing and suggestions.
+
+
+
+=cut
+
+BEGIN {
+
+    my $selected;
+    eval {
+        package Vend::Payment;
+        require Net::SSLeay;
+        import Net::SSLeay qw(post_https make_form make_headers);
+        $selected = "Net::SSLeay";
+    };
+
+    $Vend::Payment::Have_Net_SSLeay = 1 unless $@;
+
+    unless ($Vend::Payment::Have_Net_SSLeay) {
+
+        eval {
+            package Vend::Payment;
+            require LWP::UserAgent;
+            require HTTP::Request::Common;
+            require Crypt::SSLeay;
+            require CGI;
+            require Encode;
+             import Encode qw(encode decode);
+             import HTTP::Request::Common qw(POST);
+            $selected = "LWP and Crypt::SSLeay";
+        };
+
+        $Vend::Payment::Have_LWP = 1 unless $@;
+    }
+
+    unless ($Vend::Payment::Have_Net_SSLeay or $Vend::Payment::Have_LWP) {
+        die __PACKAGE__ . " requires Net::SSLeay or Crypt::SSLeay";
+    }
+
+::logGlobal("%s 0.8.7a payment module initialised, using %s", __PACKAGE__, $selected)
+        unless $Vend::Quiet;
+
+}
+
+package Vend::Payment;
+
+sub sagepay {
+	
+	my $sagepaystart = time;
+	my $date = $Tag->time({ body => "%Y%m%d%H%M%S" });
+	my $sagepaydate = $Tag->time({ body => "%A %d %B %Y, %k:%M:%S, %Z" });
+	
+	my ($vendor, $amount, $actual, $opt, $sagepayrequest);
+	
+	# Amount sent to SagePay, in 2 decimal places with cruft removed.
+	# Defaults to 'amount' from log_transaction or an invoicing system, falling back to IC input
+	   $amount =  $::Values->{amount} || charge_param('amount') || Vend::Interpolate::total_cost();
+	   $amount =~ s/^\D*//g;
+	   $amount =~ s/\,//g;
+	   $amount =  sprintf '%.2f', $amount;
+	
+	# Transaction type sent to SagePay.
+	my $txtype      = $::Values->{transtype} || charge_param('txtype') || $::Variable->{MV_PAYMENT_TRANSACTION} ||'PAYMENT';
+	my $vpsprotocol = '2.23';
+	my $accountType = $::Values->{account_type} || charge_param('account_type') || 'E';
+	my $payID       = $::Values->{inv_no} || $::Session->{mv_transaction_id} || $::Session->{id}.$amount;
+	my $logorder    = charge_param('logorder') || 'no'; # Set to 'yes' or '1' to log basket plus data useful when arguing with SagePay over empty responses
+	my $logsagepay  = charge_param('logsagepay') || 'no'; # Set to yes or 1 to log sagepay activity for debugging
+	my $logzero     = charge_param('logzero')   || 'no';
+	my $available   = $::Values->{available} || charge_param('available')  || 'no';
+	my $description = "$::Values->{company} $::Values->{fname} $::Values->{lname}";
+	   $description = substr($description,0,99);
+	my $apply3ds    = $::Values->{apply3ds} ||  charge_param('apply3ds') || '0'; # '2' will turn 3ds off, '0' is default live variant
+	my $applyAVSCV2 = $::Values->{applyavscv2} || charge_param('applyavscv2') || '0';
+	my $termurl     = charge_param('returnurl') || "$::Variable->{SECURE_SERVER}$::Variable->{CGI_URL}/ord/tdsreturn";
+	my $tdscallback = charge_param('tdscallback') || '/gateway/service/direct3dcallback.vsp';
+	my $checkouturl = charge_param('checkouturl') || "$::Variable->{SECURE_SERVER}$::Variable->{CGI_URL}/ord/checkout";
+	my $checkstatus = charge_param('check_status') || '1';
+	my $checkstatusurl = charge_param('check_status_url') || '/TxStatus/TxStatus.asp';
+	my $allowmaestro   = charge_param('allowmaestro'); # Allow Maestro card transactions to be logged offline - you will need a MOTO a/c to convert to online without 3DS.
+#::logDebug("SP".__LINE__.": apply3ds=$apply3ds; avscv2=$applyAVSCV2; txtype=$txtype. $::Values->{transtype}, $tx");
+		
+		$::Scratch->{tds} = '';
+		$::Scratch->{mstatus} = '';
+		$::Values->{amount} = '';
+		$::Values->{transtype} = '';
+		$::Values->{inv_no} = '';
+		$::Values->{apply3ds} = '';
+		$::Values->{applyavsvc2} = '';
+		$::Values->{account_type} = '';
+		undef $billingState;
+		undef $deliveryState;
+	
+	my %result;
+	my %query;
+	
+	my (%actual) = map_actual();
+		$actual  = \%actual;
+		$opt     = {};
+	
+		$vendor   = $opt->{id} || charge_param('id') || $::Variable->{MV_PAYMENT_ID};
+		$opt->{host} = charge_param('host') || $::Variable->{MV_PAYMENT_HOST} || 'live.sagepay.com';
+		$sagepayrequest = $opt->{sagepayrequest} = charge_param('sagepayrequest') || 'post';
+		$opt->{use_wget} = charge_param('use_wget') || '1';
+		$opt->{port}   = '443';
+#::logDebug("SP".__LINE__.": host=$opt->{host}; spreq=$sagepayrequest");
+	
+	if ($txtype =~ /DEFERRED|PAYMENT|AUTHENTICATE|DEFAUTH/i) {
+		$opt->{script} = '/gateway/service/vspdirect-register.vsp';
+				}
+	elsif ($txtype =~ /RELEASE/i) {
+		$opt->{script} = '/gateway/service/release.vsp';
+				}
+	elsif ($txtype =~ /DIRECTREFUND/i) {
+		$opt->{script} = '/gateway/service/directrefund.vsp';
+		}
+	elsif ($txtype =~ /REFUND/i) {
+		$opt->{script} = '/gateway/service/refund.vsp';
+		}
+	elsif ($txtype =~ /VOID/i) {
+		$opt->{script} = '/gateway/service/void.vsp';
+		}
+	elsif ($txtype =~ /CANCEL/i) {
+		$opt->{script} = '/gateway/service/cancel.vsp';
+		}
+	elsif ($txtype =~ /ABORT/i) {
+		$opt->{script} = '/gateway/service/abort.vsp';
+		}
+	elsif ($txtype =~ /MANUAL/i) {
+		$opt->{script} = '/gateway/service/manualpayment.vsp';
+		}
+	elsif ($txtype =~ /REPEAT|REPEATDEFERRED/i) {
+		$opt->{script} = '/gateway/service/repeat.vsp';
+		}
+	elsif ($txtype =~ /AUTHORISE/i) {
+		$opt->{script} = '/gateway/service/authorise.vsp';
+		}
+	
+	
+		my @override = qw/
+							order_id
+							mv_credit_card_exp_month
+							mv_credit_card_exp_year
+							mv_credit_card_start_month
+							mv_credit_card_start_year
+							mv_credit_card_issue_number
+							mv_credit_card_number
+						/;
+		for(@override) {
+			next unless defined $opt->{$_};
+			$actual->{$_} = $opt->{$_};
+		}
+	
+#::logDebug("SP".__LINE__." actual map result: " . ::uneval($actual));
+	
+	my $pan = $actual->{mv_credit_card_number} unless $pan;
+ 	   $pan =~ s/\D//g;
+	   $actual->{mv_credit_card_exp_month}    =~ s/\D//g;
+	   $actual->{mv_credit_card_exp_year}     =~ s/\D//g;
+	   $actual->{mv_credit_card_exp_year}     =~ s/\d\d(\d\d)/$1/;
+  	
+	my $exp  = sprintf '%02d%02d',
+			$actual->{mv_credit_card_exp_month}, $actual->{mv_credit_card_exp_year};
+	
+	my $expshow = $exp;
+	   $expshow =~ s/(\d\d)(\d\d)/$1\/$2/;
+	
+	my $cardType  = $actual->{mv_credit_card_type} || $CGI->{mv_credit_card_type} || $::Values->{mv_credit_card_type} unless $cardType;
+	   $cardType = 'MC' if ($cardType =~ /mastercard/i);
+	
+	my $mvccStartMonth = $actual->{mv_credit_card_start_month} || $::Values->{mv_credit_card_start_month} || $::Values->{start_date_month};
+	   $mvccStartMonth =~ s/\D//g;
+	
+	my $mvccStartYear = $actual->{mv_credit_card_start_year} || $::Values->{mv_credit_card_start_year} || $::Values->{start_date_year};
+	   $mvccStartYear =~ s/\D//g;
+	   $mvccStartYear =~ s/\d\d(\d\d)/$1/;
+	
+	my $mvccStartDate;
+		if ($mvccStartMonth == '') { $mvccStartDate = '';
+			}
+		else { $mvccStartDate = sprintf '%02d%02d', $mvccStartMonth, $mvccStartYear;
+		}
+	
+	my $issue = $actual->{mv_credit_card_issue_number} || $::Values->{mv_credit_card_issue_number} ||  $::Values->{card_issue_number};
+	   $issue =~ s/\D//g;
+	
+	my $cvv2  =  $actual->{mv_credit_card_cvv2} || $::Values->{cvv2};
+	   $cvv2  =~ s/\D//g;
+	
+	# override the configured AVSCV2/3DS settings when using a terminal
+	   $applyAVSCV2 = '2' unless ($txtype =~ /PAYMENT|DEFERRED|AUTHORISE/i);
+	   $apply3ds = '2' unless ($txtype =~ /PAYMENT|DEFERRED|AUTHENTICATE/i);
+	
+# State must be only 2 letters, and is only required for the US. Other required fields default to a space to keep Sagepay happy.
+# Filtering is now strict as Sagepay are making arbitrary changes to what they deem acceptable. 
+	my $cardHolder         = "$actual->{b_fname} $actual->{b_lname}" || "$actual->{fname} $actual->{lname}";
+	   $cardHolder         =~ s/[^a-zA-Z0-9,. ]//gi;
+	my $billingSurname     = $actual->{b_lname} || $actual->{lname};
+	   $billingSurname     =~ s/[^a-zA-Z0-9,. ]//gi;
+	my $billingFirstnames  = $actual->{b_fname} || $actual->{fname};
+	   $billingFirstnames  =~ s/[^a-zA-Z0-9,. ]//gi;
+	my $billingCountry     = $actual->{b_country} || $actual->{country} || 'GB';
+	   $billingCountry     = 'GB' if ($billingCountry =~ /UK/);
+	my $billingAddress1    = $actual->{b_address} || $actual->{address} || ' ';
+	   $billingAddress1    =~ s/\// /g;
+	   $billingAddress1    =~ s/[^a-zA-Z0-9,. ]//gi; 
+	my $billingAddress2    = $actual->{b_address2};
+	   $billingAddress2    =~ s/[^a-zA-Z0-9,. ]//gi;
+	my $billingPostCode    = $actual->{b_zip} || $actual->{zip} || ' ';
+	   $billingPostCode    =~ s/[^a-zA-Z0-9 ]//gi;
+	my $billingState       = $actual->{b_state} || $actual->{state} || '';
+	my $billingCity        = $actual->{b_city} || $actual->{city} || ' ';
+	   $billingCity       .= ", $billingState" unless $billingCountry =~ /US/i;
+	   $billingCity        =~ s/[^a-zA-Z0-9,. ]//gi;
+			undef $billingState unless  $billingCountry =~ /US/i;
+	my $billingPhone	   = $actual->{b_phone} || $actual->{phone_day} || $actual->{phone_night};
+	   $billingPhone       =~ s/[\(\)]/ /g;
+	   $billingPhone       =~ s/[^0-9-+ ]//g;
+	
+	my $deliverySurname    = $actual->{lname};
+	   $deliverySurname    =~ s/[^a-zA-Z0-9,. ]//gi;
+	my $deliveryFirstnames = $actual->{fname};
+	   $deliveryFirstnames =~ s/[^a-zA-Z0-9,. ]//gi;
+	my $deliveryCountry    = $actual->{country} || 'GB';
+	   $deliveryCountry    = 'GB' if ($deliveryCountry =~ /UK/);
+	my $deliveryPostCode   = $actual->{zip} || ' ';
+	   $deliveryPostCode   =~ s/[^a-zA-Z0-9 ]//gi;
+	my $deliveryAddress1   = $actual->{address} || ' ';
+	   $deliveryAddress1   =~ s/\// /gi;
+	   $deliveryAddress1   =~ s/[^a-zA-Z0-9,. ]//gi;
+	my $deliveryAddress2   = $actual->{address2};
+	   $deliveryAddress2   =~ s/[^a-zA-Z0-9,. ]//gi;
+	my $deliveryState      = $actual->{state} || '';
+	my $deliveryCity       = $actual->{city} || ' ';
+	   $deliveryCity      .= ", $deliveryState" unless $deliveryCountry =~ /US/i;
+	   $deliveryCity       =~ s/[^a-zA-Z0-9,. ]//gi;
+			undef $deliveryState unless $deliveryCountry =~ /US/i;
+	my $deliveryPhone      = $actual->{phone_day} || $actual->{phone_night};
+	   $deliveryPhone      =~ s/[\(\)]/ /g;
+	   $deliveryPhone      =~ s/[^0-9-+ ]//g;
+	
+	my $customerEmail      = $actual->{email};
+	   $customerEmail      =~ s/[^a-zA-Z0-9.\@\-_]//gi;
+	my $contactFax         = $::Values->{fax} || '';
+	   $contactFax         =~ s/[\(\)]/ /g;
+	   $contactFax         =~ s/[^0-9-+ ]//gi;
+	my $giftAidPayment     = $::Values->{giftaidpayment} || charge_param('giftaidpayment') || '0';
+	my $authCode           = $::Values->{authcode} || '';
+	my $clientIPAddress    = $CGI::remote_addr if $CGI::remote_addr;
+			$::Values->{authcode} = '';
+	
+::logDebug("SP".__LINE__.": bCity=$billingCity; vendorTxCode=$vendorTxCode; mvccType=$cardType; start=$mvccStartDate; issue=$issue;");
+		
+# ISO currency code sent to SagePay, from the page or fall back to config files.
+	my $currency = $::Values->{iso_currency_code} || $::Values->{currency_code} || $Vend::Cfg->{Locale}{iso_currency_code} ||
+						 charge_param('currency') || $::Variable->{MV_PAYMENT_CURRENCY} || 'GBP';
+	
+	my $psp_host = $opt->{host};
+	my $convertoffline = charge_param('convertoffline');
+
+#--- make the initial request to SagePay and get back the values for the ACS ---------------------------
+if ($sagepayrequest eq 'post') {
+   	$::Session->{sagepay}{CardRef}   = $pan;
+   	$::Session->{sagepay}{CardRef}   =~ s/^(\d\d).*(\d\d\d\d)$/****$2/;
+# vendorTxCode generated here in 'post', and retrieved from session later
+   	$order_id  = $Tag->time({ body => "%Y%m%d%H%M%S" }); 
+   	$order_id .= $::Session->{id};
+	if ($txtype   =~ /RELEASE|VOID|ABORT|CANCEL/i) {
+   		$::Session->{sagepay}{vendorTxCode} = $::Values->{OrigVendorTxCode}
+       	}
+	else {
+   		$::Session->{sagepay}{vendorTxCode} = $order_id
+	}
+
+   %query   =    (
+                    TxType                      => $txtype,
+                    Vendor                      => $vendor,
+                    AccountType                 => $accountType,
+                    VPSProtocol                 => $vpsprotocol,
+                    Apply3DSecure               => $apply3ds
+                 );
+
+	if ($txtype =~ /RELEASE/i) {
+						$query{ReleaseAmount}       = $amount;
+		}
+	
+	if ($txtype =~ /REFUND|REPEAT|AUTHORISE/) {
+					$query{RelatedVPSTxID}      = $::Values->{RelatedVPSTxID};
+					$query{RelatedVendorTxCode} = $::Values->{RelatedVendorTxCode};
+					$query{RelatedSecurityKey}  = $::Values->{RelatedSecurityKey};
+					$query{Description}         = $description;
+					$query{Amount}              = $amount;
+		}
+	
+	if ($txtype =~ /REFUND|REPEAT/) {
+					$query{RelatedTxAuthNo}     = $::Values->{RelatedTxAuthNo};
+					$query{Currency}            = $currency;
+		}
+	
+	if ($txtype =~ /VOID|ABORT|CANCEL|RELEASE/i) {
+					$query{VPSTxId}             = $::Values->{OrigVPSTxID};
+					$query{SecurityKey}         = $::Values->{OrigSecurityKey};
+		}
+	
+	if ($txtype =~ /VOID|ABORT|RELEASE/i) {
+					$query{TxAuthNo}            = $::Values->{OrigTxAuthNo};
+		}
+	
+	if ($txtype =~ /DIRECTREFUND|DEFERRED|PAYMENT|AUTHENTICATE|MANUAL/i) {
+					$query{CardType}            = $cardType;
+					$query{CardNumber}          = $pan;
+					$query{CardHolder}          = $cardHolder;
+					$query{Description}         = $description;
+					$query{Amount}              = $amount;
+					$query{Currency}            = $currency;
+					$query{ExpiryDate}          = $exp;
+		}
+	
+	if ($txtype =~ /PAYMENT|DEFERRED|AUTHENTICATE|MANUAL/i) {
+					$query{BillingFirstNames}   = $billingFirstnames;
+					$query{BillingSurname}      = $billingSurname;
+					$query{BillingAddress1}     = $billingAddress1;
+					$query{BillingAddress2}     = $billingAddress2 if $billingAddress2;
+					$query{BillingCity}         = $billingCity;
+					$query{BillingPostCode}     = $billingPostCode;
+					$query{BillingState}        = $billingState if $billingState;
+					$query{BillingCountry}      = $billingCountry;
+					$query{BillingPhone}        = $billingPhone;
+					$query{DeliveryFirstNames}  = $deliveryFirstnames;
+					$query{DeliverySurname}     = $deliverySurname;
+					$query{DeliveryAddress1}    = $deliveryAddress1;
+					$query{DeliveryAddress2}    = $deliveryAddress2 if $deliveryAddress2;
+					$query{DeliveryCity}        = $deliveryCity;
+					$query{DeliveryPostCode}    = $deliveryPostCode;
+					$query{DeliveryState}       = $deliveryState if $deliveryState;
+					$query{DeliveryCountry}     = $deliveryCountry;
+					$query{DeliveryPhone}       = $deliveryPhone;
+					$query{ContactFax}          = $contactFax;
+					$query{CustomerEmail}       = $customerEmail;
+					$query{GiftAidPayment}      = $giftAidPayment;
+					$query{ClientIPAddress}     = $clientIPAddress;
+					$query{CV2}                 = $cvv2;
+		}
+	
+	if ($txtype =~ /PAYMENT|DEFERRED|AUTHORISE/i) {
+					$query{ApplyAVSCV2}         = $applyAVSCV2;
+		}
+	
+					$query{AuthCode}            = $authCode if $authCode;
+					$query{StartDate}           = $mvccStartDate if $mvccStartDate;
+					$query{IssueNumber}         = $issue if $issue; 
+	
+	}
+
+#--- return from ACS  ------------------------------------------------------------------------
+ elsif ($sagepayrequest eq '3dsreturn') {
+  		$::Values->{sagepayrequest} = '';
+        $opt->{script} = $tdscallback;
+        $result{PaRes} = $CGI->{'PaRes'};
+        $result{MD}    = $CGI->{'MD'};
+
+#::logDebug("SP".__LINE__.": New PaRes=$result{PaRes}\nMD=$result{MD}");
+         
+         %query = (
+                    MD                => $result{MD},
+                    PaRes             => $result{PaRes}
+                  );
+  
+  }
+
+#--- query status from admin panel ----------------------------------------------------
+ elsif ($sagepayrequest eq 'querystatus') {
+   my %statusquery = (
+                      Vendor          => $vendor,
+                       VendorTxCode    => charge_param('vendortxcode')
+                     );
+       
+		$opt->{script} = $checkstatusurl;
+		my $post       = post_data($opt, \%statusquery);
+		my $response   = $post->{status_line};
+		my $page       = $post->{result_page};
+
+#::logDebug("SP".__LINE__.": query page = $page\n");
+	
+	for my $line (split /\r\n/, $page) {
+		$result{Status}       = $1 if ($line =~ /^Status=(.*)/i); 
+		$result{StatusDetail} = $1 if ($line =~ /StatusDetail=(.*)/i);
+		$result{TxType}       = $1 if ($line =~ /TransactionType=(.*)/i);
+		$result{Authorised}   = $1 if ($line =~ /Authorised=(.*)/i);
+		$result{TxAuthCode}   = $1 if ($line =~ /VPSAuthCode of (\d+)/i);
+		$result{VPSTxId}      = $1 if ($line =~ /VPSTxId=(.*)/i);
+		$result{Amount}       = $1 if ($line =~ /Amount=(.*)/i);
+		$result{Currency}     = $1 if ($line =~ /Currency=(.*)/i);
+		$result{ReceivedDate} = $1 if ($line =~ /Received=(.*)/i);
+		$result{BatchID}      = $1 if ($line =~ /BatchID=(.*)/i);
+		$result{Settled}      = $1 if ($line =~ /Settled=(.*)/i);
+		}
+ }
+
+#--- common stuff again --------------------------------------------------------------
+
+my ($post, $response, @page);
+
+# Test for gateway availability, and if not available optionally go off-line and complete
+# transaction for manual processing later. Also go off-line if amount is zero, so as to log the
+# transaction and email a receipt for audit purposes (useful mainly for subscription billing).
+
+	my ($request, $in);
+
+#::logDebug("SP".__LINE__.": available=$available, amount=$amount, order_route=$::Values->{mv_order_route}, logzero=$logzero");
+
+if (($available =~ /y|1/i) and ($amount > 0) and ($::Values->{mv_order_route} !~ /ptipm_route/i)) {
+ 	my $CMD = '/usr/bin/wget -nv --spider -T9 -t1';
+ 		open (IN, "$CMD https://$psp_host 2>&1 |") || die "Could not open pipe to wget: $!\n";
+   			$in = <IN>;
+   			chop($in);
+ 		close(IN);
+ 
+  		$in = 'test' if ($::CGI->{offline} eq 'yes'); # testing only, will force offline mode
+   		if ($in =~ /^200 OK$/)  { 
+   			$request = 'psp'; 
+   			}
+   		else { 
+   			$request = 'offline'; 
+   			}
+   		if (($cardType =~ /MAESTRO|SWITCH/i) and ($allowmaestro != '1')) {
+   			$request = 'psp'; # Maestro must go through 3D Secure unless merchant a/c type is set to MOTO in a terminal
+   			}
+    	}
+	elsif (($::Values->{mv_order_route} =~ /ptipm_route/)  and ($amount > 0)) {
+    	$request = 'psp';
+    	}
+	elsif (($available !~ /y|1/i) and ($amount > 0)) {
+    	$request = 'psp';
+    	}
+	elsif (($amount == 0) and ($logzero =~ /y|1/i)) {
+    	$request = 'log';
+}
+	 
+	 $result{VendorTxCode} = $::Session->{sagepay}{vendorTxCode};
+#::logDebug("SP".__LINE__.": in=$in; request=$request; cardtype=$cardType; vendorTxCode=$result{VendorTxCode}");
+
+if ($request eq 'psp') {
+# Run normal routine to SagePay
+
+		$query{VendorTxCode} = $::Session->{sagepay}{vendorTxCode};
+
+#::logDebug("SP".__LINE__.": now for keys in query");
+  my @query;
+    	foreach $key (sort keys(%query)) {
+    	::logDebug("Query to SagePay: \"$key=$query{$key}\""); # nicely readable version of the string sent
+       	push @query, "$key=$query{$key}";
+    	}
+  
+  my $string = join '&', @query; # replicates the string as actually sent: useful to quote this for debugging
+#::logDebug("SP".__LINE__.": string to SagePay: $string");
+		
+		$post     = post_data($opt, \%query);
+		$response = $post->{status_line};
+		$page     = $post->{result_page};
+
+::logDebug("SP".__LINE__.": response page:\n-------------------------\n$page \n---------------------------\nend of SagePay results page\n\n");
+		
+		$result{TxType}         = $txtype;
+		$result{Currency}       = $currency;
+		$result{CardRef}        = $::Session->{sagepay}{CardRef};
+		$result{CardType}       = $cardType;
+		$result{ExpAll}         = $exp;
+		$result{amount}         = $amount;
+		$result{request}        = $request;
+		$result{IP}             = $clientIPAddress;
+		$result{CardType}       = 'Electron' if ($cardType eq 'UKE');
+		$result{CardInfo}       = "$result{CardType}, $result{CardRef}, $expshow";
+	
+# Find results off returned page
+	for my $line (split /\r\n/, $page) {
+		$result{VPSTxID}        = $1 if ($line =~ /VPSTxID=(.*)/i); 
+	    $result{Status}         = $1 if ($line =~ /^Status=(.*)/i); 
+ 	    $result{StatusDetail}   = $1 if ($line =~ /StatusDetail=(.*)/i);
+ 	    $result{TxAuthNo}       = $1 if ($line =~ /TxAuthNo=(.*)/i);
+		$result{SecurityKey}    = $1 if ($line =~ /SecurityKey=(.*)/i);
+		$result{AVSCV2}         = $1 if ($line =~ /AVSCV2=(.*)/i);
+		$result{VPSProtocol}    = $1 if ($line =~ /VPSProtocol=(.*)/i);
+		$result{AddressResult}  = $1 if ($line =~ /AddressResult=(.*)/i);
+		$result{PostCodeResult} = $1 if ($line =~ /PostCodeResult=(.*)/i);
+		$result{CV2Result}      = $1 if ($line =~ /CV2Result=(.*)/i);
+	
+# and the 3DSecure results too
+		$result{SecureStatus}   = $1 if ($line =~ /SecureStatus=(.*)/i);
+		$result{MD}             = $1 if ($line =~ /MD=(.*)/i);
+		$result{ACSURL}         = $1 if ($line =~ /ACSURL=(.*)/i);
+		$result{PaReq}          = $1 if ($line =~ /PaReq=(.*)/i);
+		$result{CAVV}           = $1 if ($line =~ /CAVV=(.*)/i);
+		}
+		
+		$result{StatusDetail} =~ s/, vendor was .*/\./;
+#::logDebug("SP".__LINE__.": cardRef=$result{CardRef}; txtype=$txtype; status=$result{Status}; detail=$result{StatusDetail}; securestatus=$result{SecureStatus}");
+
+if ($txtype =~ /PAYMENT|DEFERRED|MANUAL|AUTHENTICATE/i) {
+  
+  if ($result{Status} =~ /3DAUTH/i) {
+#::logDebug("SP".__LINE__.": started status=3DAUTH");
+## Use scratch values below to populate form inside iframe. This gets the page from the bank and re-populates
+## the iframe. This page is then replaced with the 'tdsreturn' page, which shows the error message
+## if there is an error, otherwise it does not show at all but is silently replaced with the receipt.
+
+		$::Scratch->{acsurl}  = $result{ACSURL};
+		$::Scratch->{pareq}   = $result{PaReq};
+		$::Scratch->{termurl} = $termurl;
+		$::Scratch->{md}      = $result{MD};
+
+	 my $sagepayfinal = $Tag->area({ href => "ord/tdsfinal" });
+$Tag->tag({ op => 'header', body => <<EOB });
+Status: 302 moved
+Location: $sagepayfinal
+EOB
+      }
+
+  elsif ($result{Status} =~ /OK|REGISTERED|AUTHENTICATED|ATTEMPTONLY/i) {
+#::logDebug("SP".__LINE__.": reStatus=$result{Status}; securestatus=$result{SecureStatus}"); 
+  	  if ($result{SecureStatus} =~ /NOTAUTHED/i) {
+  		 $result{MStatus} = $result{'pop.status'} = 'failed';
+   		 $::Scratch->{mstatus} = 'failed';
+   		 $::Scratch->{tds} = '';
+   		 		}
+   	  else {
+		 $result{MStatus} = $result{'pop.status'} = 'success' unless $convertoffline;
+  		 $result{'order-id'} ||= $::Session->{sagepay}{vendorTxCode} unless $convertoffline;
+  		 $result{'Terminal'} = 'success' if $convertoffline;
+  		 $result{'TxType'} = uc($txtype);
+ 		 $result{'Status'} = 'OK';
+   		 $::Scratch->{mstatus} = 'success';
+   		 $::Scratch->{order_id} = $result{'order-id'};
+   		 $::Values->{mv_payment} = "Real-time card $result{CardInfo}";
+         $::Values->{psp} = charge_param('psp') || 'SagePay';
+         $::CGI::values{mv_todo} = 'submit';
+         	if ($result{SecureStatus} =~ /OK|ATTEMPTONLY/i) {
+ 	     		$::Scratch->{tds} = 'yes' ;
+ 	     		$Vend::Session->{payment_result} = \%result;
+
+::logDebug("SP".__LINE__.": secureStatus=$result{SecureStatus} so now to run routes; result hash=".::uneval(\%result));
+
+ 	     		Vend::Dispatch::do_process();
+ 	     		}
+ 	     	}
+#::logDebug("SP".__LINE__.": 3ds=$apply3ds; resStatus=$result{Status}; resSecureStatus=$result{SecureStatus}, mStatus= $result{MStatus}, orderid=$result{'order-id'}; tds=$::Scratch->{tds}\n");
+		}
+
+  elsif ($result{Status} =~ /NOTAUTHED/i) {
+		$result{MStatus} = $result{'pop.status'} = 'failed';
+		$::Scratch->{MStatus} = 'failed';
+     	if ($result{StatusDetail} =~ /AVS/i) {
+     		$::Session->{errors}{Address} =  "Data mismatch<br>Please ensure that your address matches the address on your credit-card statement, and that the card security number matches the code on the back of your card\n";
+              }
+     	else {
+     		$::Session->{errors}{Payment} = "$result{StatusDetail} $result{Status}";
+            }
+#::logDebug("SP".__LINE__.": res status = $result{Status}; MSt: $result{MStatus} or $::Session->{payment_result}{MStatus}\n");
+      	return;
+      }
+
+  elsif ($result{Status} =~ /\w+/) {
+#::logDebug("SP".__LINE__.": resStatus (other) $result{Status}");
+   		$result{MStatus} = $result{'pop.status'} = 'failed';
+   		$::Scratch->{MStatus} = 'failed';
+   		$result{MErrMsg} = $result{StatusDetail};
+     	$::Session->{errors}{Payment} = "$result{Status} $result{StatusDetail} The address and/or security code entered do not match those on file.";
+   		return;
+ 	  }
+ 
+  elsif (!$result{Status}) {
+#::logDebug("SP".__LINE__.": status=$result{Status}");
+  # If the status response is read as empty, the customer will try to repeat the order. This next
+  # Will try the 'check status' function first, but if the problem is that Sagepay are timing out 
+  # then this may also timeout. If 'check status' is positive the transaction will be processed as 
+  # though this glitch hadn't happened, otherwise it will be processed as a failure, thus preventing any 
+  # attempt to repeat the order.
+  		
+		$logorder = '1';
+		# Also log SagePay details for discussions with them
+		$logsagepay = '1';
+ 	
+ 	my %statusquery = (
+                      Vendor          => $vendor,
+                      VendorTxCode    => $::Session->{sagepay}{vendorTxCode}
+                      );
+       $opt->{script} = $checkstatusurl;
+    my $post     = post_data($opt, \%statusquery);
+    my $response = $post->{status_line};
+    my $page     = $post->{result_page};
+
+#::logDebug("SP".__LINE__.": checkstatus=$checkstatus; page=$page");
+
+		for my $line (split /\r\n/, $page) {
+    		$result{VPSTxID}    = $1 if ($line =~ /VPSTxId=(.*)/i);
+    		$result{Authorised} = $1 if ($line =~ /^Authorised=(.*)/i);
+    		$result{TxAuthNo}   = $1 if ($line =~ /VPSAuthCode=(.*)/i);
+   			 }
+#::logDebug("SP".__LINE__.": checkstatus,result=$result{Authorised}; authcode=$result{VPSAuthCode}; vtxcode=$vendorTxCode"); 
+
+       unless ($result{Authorised} =~ /YES/i) {
+my $unknown = <<EOF;
+ATTENTION: our payment processor has met an unexpected problem and we do not know if payment has
+been taken or not. Please check back with $::Variable->{COMPANY} on $::Variable->{PHONE}, quoting this 
+important reference point:
+<p>
+VendorTxCode:  $::Session->{sagepay}{vendorTxCode}
+<p>
+We apologise on behalf of our payment processor for the inconvenience
+EOF
+ 
+#::logDebug("SP".__LINE__.": $unknown\nresAuth=$result{Authorised}");
+    	 $result{MStatus} = $result{'pop.status'} = $emptysuccess;
+     	 $result{'order-id'} ||= $opt->{order_id};
+     	 $result{'TxType'} = 'NULL';
+     	 $result{'Status'} = 'UNKNOWN status - check with SagePay before dispatching goods';
+     	 $::Session->{errors}{SagePay} = $unknown;
+     			}
+       elsif($result{Authorised} =~ /YES/i) {
+#::logDebug("SP".__LINE__.": SP response was empty, now TxStatus is $result{Authorised} so force to success");
+         $result{MStatus} = $result{'pop.status'} = 'success';
+  		 $result{'order-id'} ||= $::Session->{sagepay}{vendorTxCode};
+ 		 $result{'Status'} = 'OK';
+		 $result{'MStatus'} = 'success';
+		 $::Scratch->{mstatus} = 'success';
+		 $result{'TxType'} = uc($txtype);
+		 }
+   }
+}
+
+# These next can only be done through a virtual terminal
+	elsif($txtype =~ /RELEASE|REFUND|REPEAT|AUTHORISE|DIRECTREFUND|VOID|ABORT/i ) {
+	if ($result{Status} =~ /OK/i) {
+			$result{Terminal} = 'success';
+			$result{'TxType'} = uc($txtype);
+				}
+	else {
+			$result{MStatus} = $result{'pop.status'} = 'failed';
+			$result{MErrMsg} = "$result{Status} $result{StatusDetail}";
+				}
+		}
+	}
+
+elsif ($request =~ /offline|log/i) {
+# end of PSP request, now for either OFFLINE or LOG
+  # force result to 'success' so that transaction completes
+		 $vendorTxCode = $::Session->{sagepay}{vendorTxCode};
+  		 $result{MStatus} = $result{'pop.status'} = 'success';
+  		 $result{'order-id'} ||= $::Session->{sagepay}{vendorTxCode};
+ 		 $result{'Status'} = 'OK';
+		 $result{CardRef}  = $::Session->{sagepay}{CardRef};
+		 $result{CardType} = $cardType;
+   		 $::Scratch->{mstatus} = 'success';
+   		 $::Scratch->{order_id} = $result{'order-id'};
+  		 $result{'TxType'} = uc($request);
+   		 $::Values->{mv_payment} = "Processing card $result{CardInfo}";
+         $CGI::values{mv_todo} = 'submit';
+
+#::logDebug("SP".__LINE__.": request=$request; tds=$::Scratch->{tds}");
+           
+           }
+
+     undef $request;
+     $::Values->{request} = '';
+
+	::logDebug("SP".__LINE__.":result=".::uneval(\%result));
+
+# Now extra logging for backup order and/or log of events
+	if ($logorder =~ /y|1/) {
+#--- write the full basket and address to failsafe file 
+	   $Tag->logorder('sagepay') or ::logError("SagePay: custom UserTag 'logorder.tag' not found"); # custom usertag to log full basket plus delivery details
+		}
+	
+	if (($logorder =~ /y|1/) or ($logsagepay =~ /y|1/)) {
+# Do we need this log??? Some have a keen interest in it ...
+	my $sagepayend = time;
+	my $sagepaytime = $sagepayend - $sagepaystart;
+	   chomp($sagepaytime);
+system("touch logs/sagepay.log");
+open (OUT, ">>logs/sagepay.log");
+printf OUT "
+Date of transaction = $sagepaydate
+Time to execute     = $sagepaytime seconds
+CardHolder          = $cardHolder
+Address             = $deliveryAddress1 $deliveryAddress2 $deliveryCity $deliveryState
+PostCode            = $deliveryPostCode
+Card reference      = $result{CardRef}
+Card type           = $result{CardType}
+Expiry              = $exp
+Amount              = $amount
+Currency            = $currency
+VendorTxCode        = $::Session->{sagepay}{vendorTxCode}
+SecurityKey         = $result{SecurityKey}
+VPSTxID             = $result{VPSTxID}
+TxAuthNo            = $result{TxAuthNo}
+TxType              = $result{TxType}
+Status              = $result{Status}
+AddressResult       = $result{AddressResult}
+PostCodeResult      = $result{PostCodeResult}
+CV2Result           = $result{CV2Result}
+SecureStatus        = $result{SecureStatus}
+StatusDetail        = $result{StatusDetail}
+IC result status    = $result{MStatus}
+====================================================
+";
+close(OUT);
+
+	}
+		
+	undef $apply3ds;
+	$::Values->{apply3ds} = '';
+    
+    return (%result);
+  
+}
+
+package Vend::Payment::SagePay;
+
+1;
+


hooks/post-receive
-- 
Interchange



More information about the interchange-cvs mailing list