* Add enclair_db option to UserDB.pm. Allows logging of enclair password
[interchange.git] / lib / Vend / Payment / GoogleCheckout.pm
1 # Vend::Payment::GoogleCheckout - Interchange Google Checkout support
2 #
3 # GoogleCheckout.pm, v 0.7.3, September 2009
4 #
5 # Copyright (C) 2009 Zolotek Resources Ltd. All rights reserved.
6 #
7 # Author: Lyn St George <info@zolotek.net, http://www.zolotek.net>
8 # Based on original code by Mike Heins <mheins@perusion.com> and others.
9 # This program is free software; you can redistribute it and/or modify
10 # it under the terms of the GNU General Public Licence as published by
11 # the Free Software Foundation; either version 2 of the Licence, or
12 # (at your option) any later version.
13 #
14 # This program is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17 # GNU General Public Licence for more details.
18 #
19 # You should have received a copy of the GNU General Public
20 # Licence along with this program; if not, write to the Free
21 # Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
22 # MA  02111-1307  USA.
23
24 package Vend::Payment::GoogleCheckout;
25
26 =head1 Interchange GoogleCheckout support
27
28  http://kiwi.zolotek.net is the home page with the latest version. Also to be found on
29  Kevin Walsh's excellent Interchange site,  http://interchange.rtfm.info.
30
31 =head1 AUTHORS
32
33  Lyn St George <info@zolotek.net>
34
35 =head1 CREDITS
36
37  Steve Graham, mrlock.com, debugging and documentation
38  Andy Smith, tvcables.co.uk, debugging and documentation
39
40
41 =head1 PREREQUISITES
42
43 XML::Simple and any of its prerequisites (eg XML::Parser or XML::SAX)
44 MIME::Base64
45 these should be found anyway on any well-used perl installation. This version was built especially 
46 without the Google libraries, so as to work on old machines with perl 5.6.1 and similarly vintaged
47 OSes, and only need XML::Simple. 
48
49 Interchange.cfg should contain this line, with args extra to 'rand' for current UTF8 problems:
50 SafeUntrap  rand require caller dofile print entereval
51
52 =head1 DESCRIPTION
53
54 This integrates Google Checkout quite tightly into Interchange - it is expected that all coupons,
55 gift certificates, discounts, tax routines and shipping will be processed by Interchange before
56 sending the customer to Google. The customer will see the basket, tax, shipping and total cost
57 when he arrives there. The shipping is sent as a final value, while the tax is sent as a rate for
58 Google to calculate with. The total cost is calculated by Google, and there is the possibility of
59 penny differences due to rounding errors or different rounding methods employed in different countries.
60 As Google won't accept final tax or total cost figures this cannot be helped.
61
62 This module will authorise and optionally charge the customer's card for the purchase. It will handle
63 all IPNs (notifications sent back by Google of various events in the process) and both log the results
64 and send emails to the merchant and customer as appropriate. IPNs relating to chargebacks, refunds
65 and cancellations are included in this. It can handle commands sent through an admin panel - though
66 the current IC panel is not set up to send these. Commands relating to 'charge', 'refund', 'cancel' etc
67 would normally be sent through the Google admin panel, but the resulting IPNs are handled by this module.
68
69 In the interests of tighter integration and simplicity, the consensus of opinion has been to allow
70 shipping to only the address taken by Interchange. This means that the shipping charge and tax rate
71 will be correct, but also means that the customer cannot choose another address which he may have on
72 file at Google - if he does then the system will tell him that "Merchant xx does not ship to this
73 address". He of course has the option of clicking "edit basket" and changing his address at Interchange
74 to suit. You may want to display some sort of note to this effect prior to sending the customer to
75 Google. If delivery is to a US address then the restriction is to the state and the first 3 digits of
76 the zip code; if to anywhere else in the world then the restriction is to that country and the first
77 3 characters of the postal code (or fewer characters if fewer were entered - if no postal code was
78 entered then the whole country is allowed).
79
80 It is likely that I will build another version which does allow the customer to change his address whilst at
81 Google, as this limitation is a little too harsh and not at all helpful for good customer relations. 
82
83 =head2 NOTE
84 ##########################################################################################################
85 # While you can send &amp; and have it returned safely, you cannot send &lt;, &gt; or similar in the     #
86 # description field, as it will cause IC to throw an error when Google returns the XML. Even if you send #
87 # these as &#x3c; and similar UTF-8 entities, Google will return them in the &lt; format.                #
88 #                                                                                                        #
89 # Note also that you can only send the currency that is "associated with your seller account", as        #
90 # defined by Google. There is no option to configure this, and they select it according to the country   #
91 # you registered in your sign-up account. Nor can you do recurring/subscription billing.                 #                                              #
92 ##########################################################################################################
93
94 =head1 SYNOPSIS
95
96 Go to http://checkout.google.com and set up a seller's account. They will give you a merchantid and
97 a merchantkey, which you need to enter into the payment route as described below. While there, go to
98 the "Settings" tab, then the "Integration" left menu item, and set the radio button for "XML call back",
99 and enter the URL to your callback page -  https://your_site/ord/gcoipn.
100
101 Place this module in your Vend/Payment/ directory, and call it in interchange.cfg with:
102 Require module Vend::Payment::GoogleCheckout
103
104 Add these configuration options to the new Google payment route in catalog.cfg:
105 Route googlecheckout merchantid  (as given to you by Google at sign-up)
106 Route googlecheckout merchantkey (as given to you by Google at sign-up)
107 Route googlecheckout googlehost  'https://checkout.google.com/cws/v2/Merchant' # live
108 Route googlecheckout gcoipn_url http://your_url/cgi_link/ord/gcoipn (replace 'your_url' and 'cgi_link' with yours)
109 Route googlecheckout currency    'GBP'  (or USD, or any other ISO code accepted by Google)
110 Route googlecheckout edit_basket_url (eg 'http://your_url/cgi_link/ord/basket')
111 Route googlecheckout continue_shopping_url (eg, http://your_url/cgi_link/index)
112 Route googlecheckout bypass_authorization  (1 to bypass, empty to use. See below for details)
113 Route googlecheckout default_taxrate (a decimal, eg '0.06', in case the calculation fails)
114 Route googlecheckout sender_email (email address appearing in the 'from' and 'reply-to' fields)
115 Route googlecheckout merchant_email (email address to which the order should be sent)
116 Route googlecheckout receipt_from_merchant (1 to send an email receipt to the customer)
117 Route googlecheckout email_auth_charge ('charge' to send receipt after card has been charged, 'auth' after card has been authorised)
118 Route googlecheckout html_mail ('1' to send HTML instead of plain text mail)
119 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')
120 Route googlecheckout cv2_match_accepted ('yes' for match required, 'none' for no match required. Default is 'yes')
121 Route googlecheckout default_country (2 letter ISO country code; use if not taking a delivery country at checkout, otherwise omit)
122 Route googlecheckout default_state (* if you want to allow all states within a country, or omit)
123 Route googlecheckout gco_diagnose (1 if you want to return diagnostics, empty otherwise)
124 Variable        MV_HTTP_CHARSET UTF-8
125 The last is essential, otherwise GCO will repeat the 'new_order_notification' message ad infinitum and
126 never proceed any further. You will be given no clue as to why this is happening. 
127
128 NB:/ Apache is not built by default to make the HTTP_AUTHORIZATION header available to the environment,
129 and so you will either need to rebuild it or set 'bypass_authorization' to 1 - this latter will not
130 check the returned header to see that it contains your merchantid and merchantkey. Google recommend that
131 you make this check, but it's your choice.
132
133 Add these order routes to catalog.cfg
134 Route googlecheckout <<EOF
135         attach            0
136         empty             1
137         default           1
138         supplant          1
139         no_receipt        1
140         report            etc/log_transaction
141         track             logs/tracking.asc
142         counter_tid       logs/tid.counter
143 EOF
144
145 Route gco_final master 1
146 Route gco_final cascade "copy_user main_entry"
147 Route gco_final empty   1
148 Route gco_final supplant 1
149 Route gco_final no_receipt 1
150 Route gco_final email __ORDERS_TO__
151
152 The 'edit basket' URL is available to customers when they are at Google, and lets them change either
153 the basket contents or the delivery address.
154
155 Create a GoogleCheckout button on your checkout page, including the order profile and route like so:
156   [button
157     mv_click=google
158     text="GoogleCheckout"
159     hidetext=1
160     form=checkout
161    ]
162    mv_order_profile=googlecheckout
163    mv_order_route=googlecheckout
164    mv_todo=submit
165   [/button]
166
167 Create a page in pages/ord/ called gcoipn.html, consisting of this:
168 [charge route="googlecheckout" gcorequest="callback"]
169 This page is the target of all IPN callbacks from Google, and will call the payment module in the
170 correct mode.
171
172 To have GoogleCheckout co-operate with your normal payment service provider, eg Authorizenet, do the
173 following:
174
175 Add to etc/profiles.order:
176
177 __NAME__                            googlecheckout
178 __COMMON_ORDER_PROFILE__
179 &fatal = yes
180 email=required
181 email=email
182 &set=mv_payment GCO
183 &set=psp GCO
184 &set=mv_payment_route googlecheckout
185 &set=mv_order_route googlecheckout
186 &final = yes
187 &setcheck = payment_method googlecheckout
188 __END__
189 or, if you want to use GCO as a 'Buy now' button without taking any customer details, then omit the
190 __COMMON_ORDER_PROFILE__ and the two 'email=...' lines above. Google are in fact quite finicky about
191 you not taking your customer's details, so you have the option of complying with Google or complying
192 with your own policy.
193
194 You must have MV_PAYMENT_MODE set in products/variable.txt to either your standard payment processor
195 or to 'googlecheckout'; though you may instead set this in catalog.cfg rather than variable txt as:
196 Variable MV_PAYMENT_MODE googlecheckout
197
198 Within the 'credit_card' section of etc/profiles.order leave
199 "MV_PAYMENT_MODE" 
200 as set and add
201 &set=psp __MV_PAYMENT_PSP__
202 &set=mv_payment_route authorizenet
203 (or your preferred gateway) as the last entries in the section.
204
205 and then add
206 Variable MV_PAYMENT_PSP "foo"
207 to catalog.cfg, where "foo" is the name of your gateway or acquirer, formatted as you want it to appear
208 on the receipt. Eg, "Bank of America" (rather than boa), "AuthorizeNet" (rather than authorizenet).
209
210
211 Run the following at a MySQL prompt to add the requisite fields to your transactions table:
212 (with thanks to Steve Graham)
213
214 ALTER TABLE `transactions` ADD `gco_order_number` VARCHAR( 64 ) CHARACTER SET utf8 COLLATE utf8_general_ci ,
215 ADD `gco_buyers_id` VARCHAR( 64 ) CHARACTER SET utf8 COLLATE utf8_general_ci,
216 ADD `gco_fulfillment_state` VARCHAR( 64 ) CHARACTER SET utf8 COLLATE utf8_general_ci,
217 ADD `gco_serial_number` VARCHAR( 64 ) CHARACTER SET utf8 COLLATE utf8_general_ci,
218 ADD `gco_avs_response` VARCHAR( 64 ) CHARACTER SET utf8 COLLATE utf8_general_ci,
219 ADD `gco_cvn_response` VARCHAR( 64 ) CHARACTER SET utf8 COLLATE utf8_general_ci,
220 ADD `gco_protection` VARCHAR( 64 ) CHARACTER SET utf8 COLLATE utf8_general_ci,
221 ADD `gco_cc_number` VARCHAR( 64 ) CHARACTER SET utf8 COLLATE utf8_general_ci,
222 ADD `gco_timestamp` VARCHAR( 64 ) CHARACTER SET utf8 COLLATE utf8_general_ci,
223 ADD `gco_reason` TEXT CHARACTER SET utf8 COLLATE utf8_general_ci ,
224 ADD `gco_latest_charge_amount` VARCHAR( 64 ) CHARACTER SET utf8 COLLATE utf8_general_ci,
225 ADD `gco_total_charge_amount` VARCHAR( 64 ) CHARACTER SET utf8 COLLATE utf8_general_ci,
226 ADD `gco_latest_chargeback_amount` VARCHAR( 64 ) CHARACTER SET utf8 COLLATE utf8_general_ci,
227 ADD `gco_total_chargeback_amount` VARCHAR( 64 ) CHARACTER SET utf8 COLLATE utf8_general_ci ,
228 ADD `gco_total_refund_amount` VARCHAR( 64 ) CHARACTER SET utf8 COLLATE utf8_general_ci,
229 ADD `gco_latest_refund_amount` VARCHAR( 64 ) CHARACTER SET utf8 COLLATE utf8_general_ci,
230 ADD `lead_source` VARCHAR(255) CHARACTER SET utf8 COLLATE utf8_general_ci,
231 ADD `referring_url` VARCHAR(255) CHARACTER SET utf8 COLLATE utf8_general_ci,
232 ADD `locale` VARCHAR(6) CHARACTER SET utf8 COLLATE utf8_general_ci,
233 ADD `currency_locale` VARCHAR(6) CHARACTER SET utf8 COLLATE utf8_general_ci,
234 ADD `txtype` VARCHAR(64) CHARACTER SET utf8 COLLATE utf8_general_ci,
235 ADD `cart` BLOB;
236
237 And run these to allow for temporary order numbers of greater than the default 14 character field type
238 ALTER TABLE `transactions` MODIFY `order_number` varchar(32);
239 ALTER TABLE `orderline` MODIFY `order_number` varchar(32);
240
241
242 In etc/log_transction, immediately after the
243 [elsif variable MV_PAYMENT_MODE]
244         [calc]
245 insert this line:
246         undef $Session->{payment_result}{MStatus};
247
248 and leave
249 [elsif variable MV_PAYMENT_MODE]
250 as set (contrary to earlier revisions of this document), but within the same section change the following 
251 two instances of
252 [var MV_PAYMENT_MODE] to [value mv_payment_route]
253
254 Also add these five lines to the end of the section that starts "[import table=transactions ":
255 lead_source: [data session source]
256 referring_url: [data session referer]
257 locale: [scratch mv_locale]
258 currency_locale: [scratch mv_currency]
259 cart: [calc]uneval($Items)[/calc]
260 for use when sending the merchant report and customer receipt emails out. 
261
262 Still in etc/log_transaction, find the section that starts "Set order number in values: " and insert
263 this just before it:
264 [if value mv_order_profile =~ /googlecheckout/]
265 [value name=mv_order_number set="[scratch purchaseID]" scratch=1]
266 [else]
267 and a closing [/else][/if] at the end of that section, just before the 
268 "Set order number in session:"
269 line. The order number is generated by the module and passed to Google at an early stage, and then
270 passed back to Interchange at a later stage. This prevents Interchange generating another order number.
271 If your Interchange installation is 5.2.0 or older this line will not exist - set oldic to '1' in 
272 the payment route and allow Interchange to generate the order number instead. Note: the initial order number
273 uses the username.counter number prefixed with 'GCOtmp', and a normal order number is created and the initial order number
274 replaced only when Google reports that the card has been charged. This is to avoid gaps in the order
275 number sequence caused by customers abandoning the transaction while at Google.
276
277 =over
278
279 =item Failed atttempts to authorise or charge the buyer's card.
280 If the card is declined by the bank then IC will be updated with the new status and a brief email sent
281 to the buyer telling him of the fact, and asking him to try another payment method.
282
283
284 =item AVS and CV2 risk assessment:
285 avs_match_accepted partial|full|none
286
287 AVS options and returned values are these:
288 Y - Full AVS match (address and postal code)
289 P - Partial AVS match (postal code only)
290 A - Partial AVS match (address only)
291 N - no AVS match
292 U - AVS not supported by issuer
293 If the route is set to 'full' then, unless AVS is not supported (eg in cards foreign to the country
294 doing the processing), a full match is required. Set to 'partial' (the default) for partial match, or
295 'none' for no match required.
296
297 CV2 values:
298 cv2_match_accepted  yes|none
299 M - CVN match
300 N - No CVN match
301 U - CVN not available
302 E - CVN error
303 If the route is set to 'yes' then the CV2 must match unless it is not available. If set to 'none' then
304 a match is not required. Default is 'yes'.
305
306 Both of these must be positive according to your rules for the transaction to be charged - if not positive
307 then the transaction will be refused and a brief email sent to the prospective buyer to say so.
308
309
310 =item Google Analytics
311
312 This page: http://code.google.com/apis/checkout/developer/checkout_analytics_integration.html will tell
313 you how to integrate Analytics into the system. This module will pass the data as an 'analyticsdata' 
314 value from the checkout form, encoded as UTF-8. 
315
316
317 =item Error messages from GCO
318
319 GCO will send error messages with a '<' in the title, which Interchange interprets as a possible attack
320 and so immediately stops reading the page and throws the user to the 'violation' page (defined in your
321 catalog.cfg as 'SpecialPage ../special_pages/violation' normally, though may be different).
322 Insert the following at the top of that page, which will test for the string sent by Google and then
323 bounce the user back to the checkout page with a suitable error message. This uses the 'env' UserTag.
324
325 [tmp uri][env REQUEST_URI][/tmp]
326  [if  type=explicit compare=`$Scratch->{uri} =~ /%20400%20Bad%20Request%3C\?xml/`]
327 [perl]
328         $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.");
329         $::Session->{errors}{GoogleCheckout} = $msg;
330 [/perl]
331  [bounce href="[area ord/checkout]"]
332  [/if]
333
334 =back
335
336 =head1 Bugs
337
338 The default CharSet.pm in Interchange 5.6 (and possibly earlier) will fail on GCO's notifications. The
339 sympton is that GCO keeps repeating the 'new order notification' as though it has not received one, but
340 does not return any errors. Set a variable in your catalog.cfg, thus: 
341 Variable        MV_HTTP_CHARSET UTF-8
342 but  be aware that this may break the display of some upper ASCII characters, eg the GBP £ sign (use &pound; instead of £)
343
344 =head1 Changelog
345
346 v.0.7.0, 29.01.2009
347         - added locale, currency_locale, and cart fields to transaction tbl
348         - log basket to transaction tbl to be read and inserted back into session for final order route
349         - altered main 'googlecheckout' order route and added new 'gco_final' order route. Replaced previous
350           method of sending emails with this final route. 
351         - added failsafe logging prior to going to Google, in orders/gco/, file name is 'date.session_id'
352         
353 v 0.7.1, May 2009.
354         - changed order number creation to only come after Google reports the card as charged. Initially
355           uses the tid (from tid.counter) as a temporary order number.
356
357 v0.7.2, May 2009,
358         - updated documentation, simplifed system for co-operating with other payment systems. 
359
360 v0.7.3, June 2009
361         - added code to update userdb, decrement inventory table and add more meaningful order subject (thanks to Andy Smith of tvcables.co.uk)
362         - also fixed an error whereby KDE's Kate had fooled me with incorrect bracket matching.
363                 
364 =cut
365
366 BEGIN {
367         my $selected;
368                 eval {
369                         package Vend::Payment;
370                         require XML::Simple;
371                         require LWP;
372                         require MIME::Base64;
373                         require HTTP::Request::Common;
374                         import HTTP::Request::Common qw(POST);
375                         require Net::SSLeay;
376                         require HTML::Entities;
377                         require Encode;
378                         import Encode qw(encode decode);
379                         require Data::Dumper;
380                         $selected = "XML::Simple and MIME::Base64";
381                 };
382
383                 $Vend::Payment::Have_Google = 1 unless $@;
384
385         unless ($Vend::Payment::Have_Google) {
386                 die __PACKAGE__ . " requires XML::Simple, MIME::Base64";
387         }
388
389 use XML::Simple;
390
391 ::logGlobal("%s v0.7.3 payment module initialised, using %s", __PACKAGE__, $selected) unless $Vend::Quiet;
392
393 }
394
395 package Vend::Payment;
396 use strict;
397 my ($gcourl,$merchantid,$merchantkey,$gcoserver,$xmlOut, $taxrate, $state, $header, $gcorequest, $actual, $orderID);
398
399 sub googlecheckout {
400         my ($opt, $purchaseID, $mv_order_number, $msg, $cart, %result);
401        $gcoserver   = charge_param('googlehost')  || $::Variable->{MV_PAYMENT_HOST} || 'https://checkout.google.com/api/checkout/v2'; # live
402         my $catroot     = charge_param('cat_root') || $::Variable->{CAT_ROOT};
403         my $ordersdir   = charge_param('ordersdir') || 'orders';
404         my $currency    = $::Values->{currency} || charge_param('currency') || 'GBP';   
405         my $editbasketurl = charge_param('edit_basket_url') || $::Variable->{EDIT_BASKET_URL};
406            $editbasketurl =~ s/\.html$//i;
407        $editbasketurl .= ".html?id=$::Session->{id}";
408         my $continueshoppingurl = charge_param('continue_shopping_url') || $::Variable->{CONTINUE_SHOPPING_URL};
409         my $receipturl  = charge_param('receipt_url') || $::Variable->{RECEIPT_URL};
410         my $gcoipn_url  = charge_param('gcoipn_url') || $::Variable->{GCOIPN_URL};
411         my $gcocmd_url  = charge_param('gcocmd_url') || $::Variable->{GCOCMD_URL}; # from IC admin panel, not from GCO
412         my $chargecard  = $::Values->{charge_card} || charge_param('charge_card') || '1';
413         my $basket_expiry = charge_param('basket_expiry') || $::Variable->{BASKET_EXPIRY} || '1 month';
414         my $default_taxrate = $::Values->{default_taxrate} || charge_param('default_taxrate') || '0.00';
415         my $reduced_taxrate = $::Values->{reduced_taxrate} || charge_param('reduced_taxrate') || '0.00';
416         my $taxratefield     = charge_param('taxrate_field') || 'taxrate';
417         my $reduced_taxfield = charge_param('reduced_tax_field') || 'reduced';
418         my $exempt_taxfield = charge_param('exempt_tax_field') || 'exempt';
419         my $tax_included = $::Values->{tax_included} || charge_param('tax_included') || '';
420         my $calculate_included_tax = $::Values->{calculate_included_tax} || charge_param('calculate_included_tax') || '';
421         my $ordernumber  = charge_param('ordernumber') || 'etc/order.number';
422         my $gcocounter   = charge_param('gcocounter') || 'etc/username.counter'; 
423         my $defaultshipmode = charge_param('default_shipmode') || 'upsg';
424         my $defaultcountry  = $::Values->{default_country} || charge_param('default_country') || '';
425         my $defaultstate    = $::Values->{default_state} || charge_param('default_state') || '';
426         my $bypass_auth  = charge_param('bypass_authorization') || '1';
427         my $senderemail  = charge_param('sender_email') ;
428         my $merchantemail = charge_param('merchant_email') || $::Variable->{ORDERS_TO};
429         my $doreceipt    = charge_param('receipt_from_merchant') || '1';
430         my $sendemail    = charge_param('email_auth_charge') || 'charge';
431         my $htmlmail     = charge_param('html_mail') || '';
432         my $mailriskfail = $::Values->{mailriskfail} || charge_param('mail_on_risk_failure') || "Authentication checks failed";
433         my $gcocmd       = $::Values->{gcocmd} || '';
434         my $avsmatch     = charge_param('avs_match_accepted') || 'partial';
435         my $cv2match     = charge_param('cv2_match_accepted') || 'yes';
436         my $checkouturl  = charge_param('checkouturl') || "$::Variable->{SECURE_SERVER}$::Variable->{CGI_URL}/ord/checkout";
437         my $returnurl    = charge_param('returnurl')   || "$::Variable->{SECURE_SERVER}$::Variable->{CGI_URL}/ord/gcoreceipt";
438            $returnurl    =~ s/\.html$//i;
439            $returnurl   .= ".html?id=$::Session->{id}";
440         my $diagnose     = $::Values->{gco_diagnose} || charge_param('gco_diagnose') || ''; # set to '1' to have GCO return the XML it receives for diagnostics
441         my $analytics_data = $::Values->{analyticsdata} || '';
442               $analytics_data = encode('UTF-8', $analytics_data);
443         my $tracking       = charge_param('tracking_script') || ''; 
444         my $without_address = charge_param('without_address') || ''; 
445         my $reporttitle = charge_param('reporttitle') || ''; 
446         my $dec_inventory = charge_param('decrement_inventory') || ''; # set to 1 to decrement inventory upon successful 'charge'
447         my $alwaystaxshipping = charge_param('alwaystaxshipping') || ''; # set to 1 to always tax shipping despite other config options
448
449 #----------------------------------------------------------------------------------------
450        $merchantid  = charge_param('merchantid')  || $::Variable->{MV_PAYMENT_ID};
451        $merchantkey = charge_param('merchantkey') || $::Variable->{MV_PAYMENT_SECRET};
452        $gcorequest  = charge_param('gcorequest')  || $::Values->{gcorequest} || 'post';
453            $::Values->{gcorequest} = '';
454                         
455         if ($gcorequest eq 'post') {
456                 $gcourl = "$gcoserver/merchantCheckout/Merchant/$merchantid";
457               }
458         else {
459                 $gcourl = "$gcoserver/request/Merchant/$merchantid";
460         }
461     
462         $gcourl .= "/diagnose" if ($diagnose == '1');
463 #::logDebug(":GCO:".__LINE__.": gcourl=$gcourl");
464
465 my $gco = new(
466                   merchant_id        => $merchantid,
467                   merchant_key       => $merchantkey,
468                   gco_server         => $gcourl,
469                   currency_supported => $currency
470              );
471
472 my (%actual) = map_actual();
473         $actual  = \%actual;
474         $opt     = {};
475 #::logDebug(":GCO:".__LINE__." actual map result: " . ::uneval($actual));
476
477 #----------------------------------------------------------------------------------------
478 # Initial post to GCO
479 #----------------------------------------------------------------------------------------
480 if ($gcorequest eq 'post') {
481    undef $gcorequest;
482         my $salestax = $::Values->{tax} || Vend::Interpolate::salestax() || '0.00';
483         my $shipmode = $::Values->{mv_shipmode} || charge_param('default_shipmode') || 'upsg';
484         my $shipping = $::Session->{final_shipping} || Vend::Ship::shipping($shipmode) || charge_param('default_shipping') || '0.00';
485         my $handling = $::Values->{handlingtotal} || Vend::Ship::tag_handling() || '';
486        $shipping += $handling;
487         my $shipmsg  = $::Session->{ship_message};
488         my $subtotal = $::Values->{amount} || Vend::Interpolate::subtotal();
489         my $ordertotal = charge_param('amount') || Vend::Interpolate::total_cost();
490 print "GCO".__LINE__.": tax=$salestax; shipping=$shipping, $::Values->{mv_shipping}; shipmode=$shipmode\n";
491         my $defaultcountry = charge_param('defaultcountry');
492         my $defaultstate = charge_param('defaultstate');
493         my $country  = uc($actual->{country});
494               $country  = $defaultcountry unless $country; 
495         my $state    = uc($actual->{state});
496               $state    = $defaultstate unless $state;
497         my $zip_pattern = $actual->{zip} || $::Values->{zip};
498               $zip_pattern =~ /(\S\S\S).*/;
499               $zip_pattern = "$1"."*";
500     my $taxshipping = 'false';
501               $taxshipping = 'true' if (($country =~ /$::Variable->{TAXSHIPPING}/) or ($state =~ /$::Variable->{TAXSHIPPING}/) or ($alwaystaxshipping == '1'));
502 ::logDebug(":GCO:".__LINE__.": shipping=$::Session->{final_shipping}, $shipping; handling=$handling; taxshipping=$::Variable->{TAXSHIPPING}; country=$country; tx=$taxshipping");
503  my $stax = Vend::Interpolate::salestax();
504  print "GCO:".__LINE__.": stax=$stax; mvst=$::Values->{mv_salestax}, $::Values->{salestax}\n";
505 if ($salestax == '0') { 
506          $taxrate = '0.00';
507           }
508   elsif ($taxshipping eq 'true') { 
509          $taxrate = ($salestax / ($subtotal + $shipping) || '0');
510           }
511   elsif ($calculate_included_tax == '1') {
512          $taxrate = $default_taxrate;
513           } 
514   else { 
515          $taxrate =  ($salestax / $subtotal || '0');
516 }
517 ::logDebug(":GCO:".__LINE__.": subtotal=$subtotal; taxrate=$taxrate");
518
519 ### Check that the currency sent to GCO is the one registered with them, or return to the checkout
520 my $user_currency = $::Scratch->{iso_currency_code} || $::Values->{iso_currency_code} || $currency;
521 #::logDebug(" ".__LINE__.": user currency = $user_currency, $::Scratch->{iso_currency_code}, $::Values->{iso_currency_code}; currency=$currency"); 
522  if ($user_currency ne $currency) {
523         $msg = errmsg("GoogleCheckout can take only $currency, so please reset the currency option on the page to $currency. Thank you");
524         $::Session->{errors}{GoogleCheckout} = $msg;
525  return();
526 }
527
528 #::logDebug(":GCO:".__LINE__.": ordertot=$ordertotal; subtot=$subtotal; amount=$::Values->{amount}; tax=$::Values->{tax} - $salestax; invno=$::Values->{inv_no}; zip=$zip_pattern; country=$country");
529
530 my ($item, $itm, $basket);
531 if (($::Values->{inv_no}) or ($::Values->{digital_delivery})) {
532   $shipmode = 'Digital';
533   $basket = <<EOX;
534    <item>
535     <merchant-item-id>$::Values->{inv_no}</merchant-item-id>
536         <item-name>$::Values->{inv_no}</item-name>
537         <item-description>$::Values->{notes}</item-description>
538          <quantity>1</quantity>
539         <unit-price currency="$currency">$subtotal</unit-price>
540    </item>
541 EOX
542    }
543 # TODO: allow for carts other than 'main'
544 elsif ($::Carts->{'main'}) {
545         foreach  $item (@{$::Carts->{'main'}}) {
546             $itm = {
547                         code         => $item->{'code'},
548                                 quantity     => $item->{'quantity'},
549                                 tax_category => Vend::Data::item_field($item,'tax_category'),
550                                 taxrate      => Vend::Data::item_field($item,$taxrate),
551                                 description  => Vend::Data::item_description($item),
552                                 price        => Vend::Data::item_price($item)
553                                 };
554    if ($itm->{code}){
555 # Trailing white space, raw & < > are all 'invalid xml'.
556        $itm->{code} =~ s/\s*$//g;
557        $itm->{code} =~ s/&/&#x26;/g;
558        $itm->{code} =~ s/</&#x3c;/g;
559        $itm->{code} =~ s/>/&#x3e;/g;
560        $itm->{description} =~ s/\s*$//g;
561        $itm->{description} =~ s/&\s/and /g;
562        $itm->{description} = HTML::Entities::encode_entities($itm->{description});
563        $itm->{price} =~ s/\s*$//g;
564        $itm->{price} /= (1 + ($itm->{taxrate} || $default_taxrate)) 
565                                 if ($calculate_included_tax == '1');
566        $itm->{quantity} =~ s/\s*$//g;
567   
568   if ($itm->{tax_category}) {
569   $basket .= <<EOB;
570    <item>
571     <merchant-item-id>$itm->{code}</merchant-item-id>
572         <item-name>$itm->{code}</item-name>
573         <item-description>$itm->{description}</item-description>
574         <unit-price currency="$currency">$itm->{price}</unit-price>
575     <quantity>$itm->{quantity}</quantity>
576     <tax-table-selector>$itm->{tax_category}</tax-table-selector>
577    </item>
578 EOB
579         }
580   else {
581   $basket .= <<EOB;
582    <item>
583     <merchant-item-id>$itm->{code}</merchant-item-id>
584         <item-name>$itm->{code}</item-name>
585         <item-description>$itm->{description}</item-description>
586         <unit-price currency="$currency">$itm->{price}</unit-price>
587     <quantity>$itm->{quantity}</quantity>
588    </item>
589 EOB
590       }
591          }
592    }
593  }
594 else {
595   $msg = errmsg("You must pass something to GoogleCheckout");
596         $::Session->{errors}{GoogleCheckout} = $msg;
597         return($msg);
598 }
599
600    $orderID = gen_order_id($opt);
601    $::Scratch->{orderID} = $orderID;
602    $::Scratch->{txtype} = 'GCO - PENDING';
603
604 # Disable order number creation in log_transaction and create it here instead
605 if ($::Values->{inv_no}) {
606    $purchaseID = $::Values->{inv_no};
607       }
608 elsif ($::Values->{mv_order_number}){
609   # IC 5.2 and earlier set order number prior to log_transaction
610    $purchaseID = $::Values->{mv_order_number};
611       }
612 else{
613 # Use temporary number as the initial order number, and only replace upon successful order completion
614     $purchaseID = 'GCOtmp'.Vend::Interpolate::tag_counter("$gcocounter");
615 #::logDebug(":GCO:".__LINE__.": purchaseID=$purchaseID;");
616 }
617     
618     $::Scratch->{purchaseID} = $purchaseID;
619
620 #::logDebug(":GCO:".__LINE__.": txtype=$::Scratch->{txtype};  orderid=$orderID, purchaseid=$purchaseID");
621
622 # XML to send
623 $xmlOut = <<EOX;
624 <?xml version="1.0" encoding="UTF-8"?>
625 <checkout-shopping-cart xmlns="http://checkout.google.com/schema/2">
626  <shopping-cart>
627   <merchant-private-data>
628    <merchant-note>$purchaseID</merchant-note>
629   </merchant-private-data>
630    <items>
631 EOX
632
633   $xmlOut .= $basket;
634
635   $xmlOut .= <<EOX;
636    </items>
637  </shopping-cart>
638 <checkout-flow-support>
639 <merchant-checkout-flow-support>
640  <edit-cart-url>$editbasketurl</edit-cart-url>
641   <continue-shopping-url>$continueshoppingurl</continue-shopping-url>
642    <tax-tables>
643         <default-tax-table>
644          <tax-rules>
645           <default-tax-rule>
646           <shipping-taxed>$taxshipping</shipping-taxed>
647           <rate>$taxrate</rate>
648           <tax-area>
649 EOX
650
651 if ($country =~ /US/i) {
652  $xmlOut .= <<EOX;
653                 <us-state-area>
654                  <state>$state</state>
655                 </us-state-area>
656 EOX
657    }
658 else {
659   $xmlOut .= <<EOX;
660         <postal-area>
661          <country-code>$country</country-code>
662         </postal-area>
663 EOX
664  }
665
666  $xmlOut .= <<EOX;
667            </tax-area>
668           </default-tax-rule>
669          </tax-rules>
670         </default-tax-table>
671     <alternate-tax-tables>
672      <alternate-tax-table standalone="true" name="$reduced_taxfield">
673        <alternate-tax-rules>
674          <alternate-tax-rule>
675            <rate>$reduced_taxrate</rate>
676            <tax-area>
677              <world-area/>
678            </tax-area>
679          </alternate-tax-rule>
680        </alternate-tax-rules>
681      </alternate-tax-table>
682      <alternate-tax-table standalone="true" name="$exempt_taxfield">
683        <alternate-tax-rules/>
684      </alternate-tax-table>
685    </alternate-tax-tables>
686   </tax-tables>
687 <shipping-methods>
688  <flat-rate-shipping name="$shipmode">
689   <price currency="$currency">$shipping</price>
690    <shipping-restrictions>
691         <allowed-areas>
692 EOX
693
694 if ($country =~ /US/i) {
695  $xmlOut .= <<EOX;
696         <us-state-area>
697           <state>$state</state>
698             </us-state-area>
699                 <us-zip-area>
700           <zip-pattern>$zip_pattern</zip-pattern>
701         </us-zip-area>
702 EOX
703     }
704 else {
705   $xmlOut .= <<EOX;
706          <postal-area>
707           <country-code>$country</country-code>
708            <postal-code-pattern>$zip_pattern</postal-code-pattern>
709          </postal-area>
710 EOX
711  }
712   $xmlOut .= <<EOX;
713         </allowed-areas>
714    </shipping-restrictions>
715  </flat-rate-shipping>
716 </shipping-methods>
717  <analytics-data>$analytics_data</analytics-data>
718   <parameterized-urls>
719     <parameterized-url url="$returnurl"/>
720   </parameterized-urls>
721 </merchant-checkout-flow-support>
722 </checkout-flow-support>
723 </checkout-shopping-cart>
724 EOX
725
726 #
727 # Write full order to orders/gco/ using gco$date.$session_id file name as failsafe backup in case order 
728 # route fails. 
729
730         my $date     = $Tag->time({ body => "%Y%m%d%H%M%S" });
731         my $pagefile = charge_param('report_page') || 'etc/report';
732         my $page     = readfile($pagefile);
733            $page     = interpolate_html($page) if $page;
734
735         mkdir "$ordersdir/gco", 0775 unless -d "$ordersdir/gco";
736         
737         my $fn = Vend::Util::catfile(
738                                 "$ordersdir/gco",
739                                 "gco$date.$::Session->{id}"  
740                         );
741    
742     Vend::Util::writefile( $fn, $page )
743                                 or ::logError("GCO error writing failsafe order $fn: $!");
744         
745 #--------------------------------------------------------------------------------
746 # Post the basket to GCO and read the redirect URL to which the customer is sent.
747    my $return = sendxml($xmlOut);
748    my $xml    = new XML::Simple();
749    my $xmlin  = $xml->XMLin("$return");
750           $::Session->{errors}{GoogleCheckout} = $xmlin->{'error-message'}
751                                 if $xmlin->{'error-message'};
752  
753     my $redirecturl = $xmlin->{'redirect-url'};
754         my $gco_serial_number = $xmlin->{'serial-number'};
755 #::logDebug(":GCO:".__LINE__.": return=$return, redirect=$redirecturl; gcourl=$gcourl;serial number=$gco_serial_number");
756 use Data::Dumper; # for debugging
757 # print Dumper($xmlin); # for debugging
758 #print Dumper($::Session);  
759   unless (($xmlin->{'error-message'}) or ($diagnose)) {
760           $redirecturl = Vend::Util::header_data_scrub($redirecturl);
761
762 $::Tag->tag({ op => 'header', body => <<EOB });
763 Status: 302 moved
764 Location: $redirecturl
765 EOB
766
767
768 # Fake the result so that IC can log the transaction
769    $result{Status}     = 'success';
770    $result{MStatus}    = 'success';
771    $result{'order-id'} = $orderID;
772    
773    return %result;
774       }
775  
776  }
777
778 #----------------------------------------------------------------------------------------
779 # Now handle callbacks, eg notification of payment, risk assessment, etc
780 #----------------------------------------------------------------------------------------
781
782         elsif ($gcorequest eq 'callback') {
783
784 #### First authenticate the message using the merchantid and merchantkey in the header, then
785 #### determine type of callback and respond appropriately.  Apache does not pass HTTP_AUTHORIZATION to
786 #### the environment in its default configuration for security reasons, and may need to be recompiled
787
788         my $authdata = $ENV{HTTP_AUTHORIZATION};
789         my ($id, $key, $authed, $email, $locale, $company_name, $new_order_no, $date, $phone, $sendermail);
790
791   unless ($bypass_auth == '1') {
792          if (($authdata) and (substr($ENV{HTTP_AUTHORIZATION},0,6) eq 'Basic ')) {
793         my $decoded = decode_base64(substr($ENV{HTTP_AUTHORIZATION},6));
794            if ($decoded =~ /:/) {
795               ($id, $key) = split(/:/, $decoded);
796                         if (($id eq $merchantid) and ($key eq $merchantkey)) {
797                         $authed = 'yes';
798                                 }
799                             else {
800                                 $authed = 'failed';
801                         }
802            }
803           }
804   }
805
806 if (($authed eq 'yes') or ($bypass_auth == '1')) {
807
808 # Read xml, initialise db table, create new XML object.
809         my $xmlIpn = ::http()->{entity};
810 # ::logDebug(":GCO:".__LINE__.": xmlIpn=$$xmlIpn");
811         my $db  = dbref('transactions') or die errmsg("cannot open transactions table");
812         my $dbh = $db->dbh() or die errmsg("cannot get handle for tbl 'transactions'");
813     my $sth;
814     
815     my $xml   = new XML::Simple();
816     my $xmlin = $xml->XMLin("$$xmlIpn");
817         my $gco_serial_number = $xmlin->{'serial-number'};
818
819 #--- new order notification ---------------------------------------------------------------
820 if ($$xmlIpn =~ /new-order-notification/) {
821         my $gco_order_number      = $xmlin->{'google-order-number'}; 
822         my $gco_timestamp         = $xmlin->{'timestamp'};      
823         my $gco_fulfillment_state = $xmlin->{'fulfillment-order-state'}; 
824         my $gco_financial_state   = $xmlin->{'financial-order-state'};  
825         my $email_allowed         = $xmlin->{'buyer-marketing-preferences'}->{'email-allowed'}; 
826         my $buyers_id             = $xmlin->{'buyer-id'}; 
827         my $total_tax             = $xmlin->{'order-adjustment'}->{'total-tax'}->{'content'}; 
828         my $shipping              = $xmlin->{'order-adjustment'}->{'shipping'}->{'flat-rate-shipping-adjustment'}->{'shipping-cost'}->{'content'}; 
829         my $order_total           = $xmlin->{'order-total'}->{'content'}; 
830         my $mv_order_number       = $xmlin->{'shopping-cart'}->{'merchant-private-data'}->{'merchant-note'};
831         my $company_name          = $xmlin->{'buyer-shipping-address'}->{'company-name'}
832                                        unless ($xmlin->{'buyer-shipping-address'}->{'company-name'} =~ /HASH/);
833         my $buyers_name           = $xmlin->{'buyer-shipping-address'}->{'contact-name'};
834         my $fname                 = $xmlin->{'buyer-shipping-address'}->{'structured-name'}->{'first-name'};
835         my $lname                 = $xmlin->{'buyer-shipping-address'}->{'structured-name'}->{'last-name'};
836         my $address1              = $xmlin->{'buyer-shipping-address'}->{'address1'};
837         my $address2              = $xmlin->{'buyer-shipping-address'}->{'address2'}
838                                        unless ($xmlin->{'buyer-shipping-address'}->{'address2'} =~ /HASH/);
839         my $city                  = $xmlin->{'buyer-shipping-address'}->{'city'};
840         my $state                 = $xmlin->{'buyer-shipping-address'}->{'region'}
841                                        unless ($xmlin->{'buyer-shipping-address'}->{'region'} =~ /HASH/);
842         my $postal_code           = $xmlin->{'buyer-shipping-address'}->{'postal-code'}
843                                        unless ($xmlin->{'buyer-shipping-address'}->{'postal-code'} =~ /HASH/);
844         my $country               = $xmlin->{'buyer-shipping-address'}->{'country-code'};
845         my $phone                 = $xmlin->{'buyer-shipping-address'}->{'phone'}
846                                        unless ($xmlin->{'buyer-shipping-address'}->{'phone'} =~ /HASH/);
847         my $fax                   = $xmlin->{'buyer-shipping-address'}->{'fax'}
848                                        unless ($xmlin->{'buyer-shipping-address'}->{'fax'} =~ /HASH/);
849         my $email                 = $xmlin->{'buyer-shipping-address'}->{'email'};
850         my $b_company_name        = $xmlin->{'buyer-billing-address'}->{'company-name'}
851                                        unless ($xmlin->{'buyer-billing-address'}->{'company-name'} =~ /HASH/);
852         my $b_buyers_name         = $xmlin->{'buyer-billing-address'}->{'contact-name'};
853         my $b_fname               = $xmlin->{'buyer-billing-address'}->{'structured-name'}->{'first-name'};
854         my $b_lname               = $xmlin->{'buyer-billing-address'}->{'structured-name'}->{'last-name'};
855         my $b_address1            = $xmlin->{'buyer-billing-address'}->{'address1'};
856         my $b_address2            = $xmlin->{'buyer-billing-address'}->{'address2'}
857                                        unless ($xmlin->{'buyer-billing-address'}->{'address2'} =~ /HASH/);
858         my $b_city                = $xmlin->{'buyer-billing-address'}->{'city'};
859         my $b_state               = $xmlin->{'buyer-billing-address'}->{'region'}
860                                        unless ($xmlin->{'buyer-billing-address'}->{'region'} =~ /HASH/);
861         my $b_postal_code         = $xmlin->{'buyer-billing-address'}->{'postal-code'}
862                                        unless ($xmlin->{'buyer-billing-address'}->{'postal-code'} =~ /HASH/);
863         my $b_country             = $xmlin->{'buyer-billing-address'}->{'country-code'};
864         my $b_phone               = $xmlin->{'buyer-billing-address'}->{'phone'}
865                                        unless ($xmlin->{'buyer-billing-address'}->{'phone'} =~ /HASH/);
866
867            $buyers_name =~ /(\w+)\s+(\D+)/;
868            $fname = $1 if ($fname =~ /HASH/);
869            $lname = $2 if ($lname =~ /HASH/);
870            $b_buyers_name =~ /(\w+)\s+(\D+)/;
871            $b_fname = $1 if ($b_fname =~ /HASH/);
872            $b_lname = $2 if ($b_lname =~ /HASH/);
873        
874        $postal_code =~ /^(\S\S\S).*/;
875     my $postal_code_short = $1;
876  
877 #::logDebug(":GCO:".__LINE__.": gsn=$gco_serial_number, gon=$gco_order_number, shipping=$shipping,  fname=$fname, lname=$lname, mvon=$mv_order_number");
878 # Update IC db - update total_cost here as well, in case of penny differences in rounding methods.
879           $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'");
880       $sth->execute() or die errmsg("Cannot update transactions tbl for gco '$mv_order_number'");
881     
882     }
883
884 #--- update to order ---------------------------------------------------------------------------------------
885 elsif ($$xmlIpn =~ /order-state-change-notification/) {
886         my $gco_serial_number     = $xmlin->{'serial-number'};
887         my $gco_order_number      = $xmlin->{'google-order-number'}; 
888         my $gco_timestamp         = $xmlin->{'timestamp'}; 
889         my $gco_fulfillment_state = $xmlin->{'new-fulfillment-order-state'}; 
890         my $gco_financial_state   = $xmlin->{'new-financial-order-state'}; 
891         
892            $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");
893        $sth->execute() or die errmsg("Cannot get data from transactions tbl");
894     my @d = $sth->fetchrow_array;
895     my $order_total = $d[0];
896        $email       = $d[1];
897     my $txtype      = $d[2];
898     my $old_tid     = $d[3];
899
900         unless ($txtype =~ /GCO - CHARGED/i) {
901            if ($gco_financial_state =~ /CHARGED/i) {
902               $new_order_no  = Vend::Interpolate::tag_counter("$ordernumber") unless defined $::Values->{mv_order_number}; 
903                   $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'");
904            my $stho = $dbh->prepare("UPDATE orderline SET status='processing', code=replace(code, '$old_tid', '$new_order_no'), order_number='$new_order_no' WHERE order_number='$old_tid'");
905                   $stho->execute() or die errmsg("Cannot update orderline tbl for gco '$gco_order_number'") unless defined $::Values->{mv_order_number};
906         # Decrement inventory here now that we know the transaction has succeeded
907            if ($dec_inventory == '1') {
908                 my $sthcart = $dbh->prepare("SELECT cart FROM transactions WHERE gco_order_number='$gco_order_number'") or die errmsg("Cannot select from transactions tbl for $gco_order_number");
909                    $sthcart->execute() or die errmsg("Cannot get data from transactions tbl");
910                 my $cart = $sthcart->fetchrow_array;
911                         $cart = eval ($cart);
912                 my $dbi = dbref('inventory') or die errmsg("cannot open inventory table");
913                 my $dbhi = $dbi->dbh() or die errmsg("cannot get handle for tbl 'inventory'");
914                 my ($sthi, $itm, $qty);
915
916         foreach my $items (@{$cart}) { 
917                                         $itm = $items->{'code'};
918                                         $qty = $items->{'quantity'};
919                                         $sthi = $dbh->prepare("UPDATE inventory SET quantity = quantity -'$qty' WHERE sku = '$itm'");
920                                         $sthi->execute() or die errmsg("Cannot update table inventory");
921 ::logDebug(":GCO:".__LINE__.": Decremented inventory for $itm by $qty");
922                                         }
923                                   }     
924                                 }
925                 else {
926        $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'");
927                 }
928        $sth->execute() or die errmsg("Cannot update transactions tbl for gco '$gco_order_number'") unless defined $::Values->{mv_order_number};
929 #::logDebug(":GCO:".__LINE__.": gco_finstate=$gco_financial_state; txtype=$txtype; neworderno=$new_order_no; pID=$purchaseID");    
930        }
931
932         my ($mailout, $finstatus);
933
934         if ($gco_financial_state =~ /PAYMENT_DECLINED/i) {
935         $mailout = <<EOM;
936 Card payment for Google Order number $gco_order_number from $::Variable->{COMPANY}, $order_total, was
937 declined by your bank. Please use an alternative means of payment if you wish to proceed with this order.
938 EOM
939         $finstatus = "declined by your bank";
940         }
941         elsif ($gco_financial_state =~ /CANCELLED/i) {
942                 $mailout = <<EOM;
943 Google Order number $gco_order_number from $::Variable->{COMPANY} has been cancelled.
944 EOM
945         $finstatus = "cancelled";
946         }
947
948         if ($gco_financial_state =~ /PAYMENT_DECLINED|CANCELLED/i) {
949         $::Tag->email({ to => "$email", from => "$senderemail", reply => "$senderemail", extra => "Bcc: $merchantemail",
950                 subject => "Google order $gco_order_number has been $finstatus",
951                 body => "$mailout\n"
952              });
953         }
954   }
955
956
957 #--- risk notification ---------------------------------------------------------------------------------------
958 elsif ($$xmlIpn =~ /risk-information-notification/) {
959         my $gco_serial_number = $xmlin->{'serial-number'};
960         my $gco_order_number  = $xmlin->{'google-order-number'}; 
961         my $gco_timestamp     = $xmlin->{'timestamp'}; 
962         my $gco_protection    = $xmlin->{'risk-information'}->{'eligible-for-protection'};
963         my $gco_avs_response  = $xmlin->{'risk-information'}->{'avs-response'};
964         my $gco_cvn_response  = $xmlin->{'risk-information'}->{'cvn-response'}; 
965         my $gco_cc_number     = $xmlin->{'risk-information'}->{'partial-cc-number'};
966         my $gco_account_age   = $xmlin->{'risk-information'}->{'buyer-account-age'};
967         my $gco_buyers_ip     = $xmlin->{'risk-information'}->{'ip-address'};
968
969            $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'");
970        $sth->execute() or die errmsg("Cannot update transactions tbl for gco '$gco_order_number'");
971
972 # Assess risk, and if OK then optionally tell GCO to charge the card; and send out emails.
973     my ($process_order, $avs, $cv2);
974   if (($avsmatch) eq 'full' and ($gco_avs_response =~ /Y|U/i)) {
975                 $avs = 'pass';
976                 }
977         elsif (($avsmatch) eq 'partial' and ($gco_avs_response !~ /N/i)) {
978                 $avs = 'pass';
979                 }
980         elsif ($avsmatch eq 'none') {
981                 $avs = 'pass';
982    }
983
984   if (($cv2match) eq 'yes' and ($gco_cvn_response !~ /N/i)) {
985                 $cv2 = 'pass';
986                 }
987         elsif ($cv2match eq 'none') {
988                 $cv2 = 'pass';
989   }
990  
991   if (($avs eq 'pass') and ($cv2 eq 'pass')) {
992                 if ($chargecard =~ /1|y/) { 
993     # Tell Google to charge the card
994        $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");
995        $sth->execute() or die errmsg("Cannot get data from transactions tbl");
996     my $order_total = $sth->fetchrow();
997
998     my $xmlOut = <<EOX;
999 <?xml version="1.0" encoding="UTF-8"?>
1000 <charge-order xmlns="http://checkout.google.com/schema/2" google-order-number="$gco_order_number">
1001     <amount currency="$currency">$order_total</amount>
1002 </charge-order>
1003 EOX
1004          sendxml($xmlOut) ;
1005                 }
1006         }
1007  else  {
1008 # Risk assessment fails to meet rules
1009            $::Tag->email({ to => "$email", from => "$senderemail", reply => "$sendermail", extra => "Bcc: $merchantemail",
1010                 subject => "Google order $gco_order_number declined\n\n",
1011                 body => "$mailriskfail\n"
1012              });
1013         }
1014     }
1015
1016 #--- charge amount ----------------------------------------------------------------------------------------
1017 elsif ($$xmlIpn =~ /charge-amount-notification/) {
1018         my $gco_serial_number        = $xmlin->{'serial-number'};
1019         my $gco_order_number         = $xmlin->{'google-order-number'}; 
1020         my $gco_timestamp            = $xmlin->{'timestamp'}; 
1021         my $gco_latest_charge_amount = $xmlin->{'latest-charge-amount'}->{'content'};
1022         my $gco_total_charge_amount  = $xmlin->{'total-charge-amount'}->{'content'};
1023
1024        $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,subtotal,salestax,shipping,order_date,lead_source,referring_url,txtype,locale,currency_locale,cart,username FROM transactions WHERE gco_order_number='$gco_order_number'") or die errmsg("Cannot select from transactions tbl for $gco_order_number");
1025        $sth->execute() or die errmsg("Cannot get data from transactions tbl");
1026     my @d = $sth->fetchrow_array;
1027     my $order_total = $::Values->{order_total} = $d[0];
1028     my $email = $::Values->{email} = $d[1];
1029     my $mv_order_number = $::Values->{mv_order_number} = $d[2];
1030     my $fname = $::Values->{fname} = $d[3];
1031     my $lname = $::Values->{lname} = $d[4];
1032     my $company = $::Values->{company} = $d[5];
1033     my $address1 = $::Values->{address1} = $d[6];
1034     my $address2 = $::Values->{address2} = $d[7];
1035     my $city = $::Values->{city} = $d[8];
1036     my $state = $::Values->{state} = $d[9];
1037     my $zip = $::Values->{zip} = $d[10];
1038     my $country = $::Values->{country} = $d[11];
1039     my $phone_day = $::Values->{phone_day} = $d[12];
1040     my $fax = $::Values->{fax} = $d[13];
1041     my $b_fname = $::Values->{b_fname} = $d[14];
1042     my $b_lname = $::Values->{b_lname} = $d[15];
1043     my $b_company = $::Values->{b_company} = $d[16];
1044     my $b_address1 = $::Values->{b_address1} = $d[17];
1045     my $b_address2 = $::Values->{b_address2} = $d[18];
1046     my $b_city = $::Values->{b_city} = $d[19];
1047     my $b_state = $::Values->{b_state} = $d[20];
1048     my $b_zip = $::Values->{b_zip} = $d[21];
1049     my $b_country = $::Values->{b_country} = $d[22];
1050     my $shipmode = $::Values->{shipmode} = $d[23];
1051     my $handling = $::Values->{handling} = $d[24];
1052     my $subtotal = $::Values->{subtotal} = $d[25];
1053     my $salestax = $::Values->{salestax} = $d[26];
1054     my $shipping = $::Values->{shipping} = $d[27];
1055     my $order_date = $::Values->{order_date} = $d[28];
1056     my $lead_source = $::Session->{lead_source} = $d[29];
1057     my $referring_url = $::Session->{referer} = $d[30];
1058         my $txtype = $::Values->{txtype} = $d[31];
1059         my $mv_locale = $d[32];
1060         my $mv_currency = $d[33];
1061         my $cart = $d[34];
1062         my $username = $d[35];
1063
1064                 $cart =~ s/\"/\'/g;
1065                 $cart =~ s/\\//;
1066                 $cart = eval($cart); 
1067 #::logDebug(":GCO:".__LINE__.": cart=$cart");   
1068            
1069            $::Values->{mv_payment} = 'GoogleCheckout';
1070            $::Values->{gco_order_number} = $gco_order_number;
1071            $::Session->{values}->{iso_currency_code} = $currency;
1072            $::Session->{scratch}->{mv_locale} = $mv_locale;
1073            $::Session->{scratch}->{mv_currency} = $mv_currency || $locale;
1074  
1075  # Check that the order has not already been charged, as Google sometimes send extra IPNs when they shouldn't.
1076         unless ($txtype =~ /GCO - CHARGED/i) {
1077            $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'");
1078        $sth->execute() or die errmsg("Cannot update transactions tbl for gco '$gco_order_number'");
1079        }
1080
1081 # Update the customer's details in userdb
1082           $db = dbref('userdb') or die errmsg("cannot open userdb table");
1083           $dbh = $db->dbh() or die errmsg("cannot get handle for tbl 'userdb'");
1084           $sth = $dbh->prepare("UPDATE userdb SET fname='$fname',lname='$lname',address1='$address1',address2='$address2',city='$city',state='$state',zip='$zip',country='$country',phone_day='$phone',fax='$fax',email='$email',company='$company_name' WHERE username='$username'");
1085           $sth->execute() or die errmsg("Cannot update userdb tbl for user '$username'");
1086
1087 # Add IC order number to GCO admin panel
1088     my $xmlOut = <<EOX;
1089 <?xml version="1.0" encoding="UTF-8"?>
1090 <add-merchant-order-number xmlns="http://checkout.google.com/schema/2" google-order-number="$gco_order_number">
1091     <merchant-order-number>$mv_order_number</merchant-order-number>
1092 </add-merchant-order-number>
1093 EOX
1094                 sendxml($xmlOut);
1095
1096 # Make the order number easier to correlate with Google's
1097         if ($reporttitle == '1') {
1098                 $::Values->{mv_order_subject} = 'Order '.$new_order_no.' : GCOID '.$gco_order_number.' : '.$txtype;
1099         }  
1100
1101 # run custom final route which cascades 'copy_user' and 'main_entry',  but no receipt page.
1102                 $::Values->{email_copy} = '1';
1103         Vend::Order::route_order("gco_final", $cart) if $cart;
1104
1105     }
1106
1107 #--- chargeback amount -------------------------------------------------------------------------------------
1108 elsif ($$xmlIpn =~ /chargeback-amount-notification/) {
1109         my $gco_serial_number            = $xmlin->{'serial-number'};
1110         my $gco_order_number             = $xmlin->{'google-order-number'}; 
1111         my $gco_timestamp                = $xmlin->{'timestamp'}; 
1112         my $gco_latest_chargeback_amount = $xmlin->{'latest-chargeback-amount'}->{'content'};
1113         my $gco_total_chargeback_amount  = $xmlin->{'total-chargeback-amount'}->{'content'};
1114
1115            $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'");
1116            $sth->execute() or die errmsg("Cannot update transactions tbl for gco '$gco_order_number'");
1117
1118         my $mailchargeback = <<EOM;
1119 Order $gco_order_number has had a chargeback of $gco_latest_chargeback_amount on $date, making the total
1120 chargeback for this order $gco_total_chargeback_amount.
1121 EOM
1122         $::Tag->email({ to => "$merchantemail", from => "$senderemail", reply => "$sendermail",
1123                 subject => "Google order $gco_order_number has had a CHARGEBACK",
1124                 body => "$mailchargeback\n",
1125              });
1126
1127     }
1128
1129
1130 #--- refund amount -------------------------------------------------------------------------------------
1131 elsif ($$xmlIpn =~ /refund-amount-notification/) {
1132         my $gco_serial_number        = $xmlin->{'serial-number'};
1133         my $gco_order_number         = $xmlin->{'google-order-number'}; 
1134         my $gco_timestamp            = $xmlin->{'timestamp'}; 
1135         my $gco_latest_refund_amount = $xmlin->{'latest-refund-amount'}->{'content'};
1136         my $gco_total_refund_amount  = $xmlin->{'total-refund-amount'}->{'content'};
1137
1138            $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'");
1139        $sth->execute() or die errmsg("Cannot update transactions tbl for gco '$gco_order_number'");
1140
1141         my $mailrefund = <<EOM;
1142 Order $gco_order_number has been refunded for $gco_latest_refund_amount on $date, making the total
1143 refund for this order $gco_total_refund_amount.
1144 EOM
1145         $::Tag->email({ to => "$merchantemail", from => "$senderemail", reply => "$sendermail",
1146                 subject => "Google order $gco_order_number has been refunded",
1147                 body => "$mailrefund\n",
1148              });
1149        }
1150     } 
1151  }
1152
1153 #===================================================================================================
1154 # Now deal with any commands: charge card, ship, refund etc, which might come through an admin panel
1155 #---------------------------------------------------------------------------------------------------
1156
1157 elsif ($gcorequest eq 'command') {
1158
1159         my $gco_order_number = $::Values->{gco_order_number};
1160         my $mv_order_number  = $::Values->{mv_order_number};
1161         my $amount           = $::Values->{gco_amount};
1162         my $reason           = $::Values->{gco_reason};
1163         my $carrier          = $::Values->{gco_shipping_company};
1164            $carrier = 'Other' unless ($carrier =~ /DHL|FedEx|UPS|USPS/i);
1165         my $tracking_number  = $::Values->{tracking_number};
1166         my $send_email       = $::Values->{email_text};
1167
1168 #--- charge order -----------------------------------------------------------------------------
1169 if ($gcocmd =~ /charge/) {
1170     $xmlOut = <<EOX;
1171 <?xml version="1.0" encoding="UTF-8"?>
1172 <charge-order xmlns="http://checkout.google.com/schema/2" google-order-number="$gco_order_number">
1173     <amount currency="$currency">$amount</amount>
1174 </charge-order>
1175 EOX
1176    
1177         my $return = sendxml($xmlOut);
1178         my $xml    = new XML::Simple();
1179         my $xmlin  = $xml->XMLin("$return");
1180            $::Session->{payment_result}{Terminal} = 'success' unless ($xmlin->{'error-message'});       
1181        $::Session->{errors}{GoogleCheckout} = $xmlin->{'error-message'} if ($xmlin->{'error-message'});
1182     }
1183
1184 #--- add Interchange order number --------------------------------------------------------------
1185 elsif ($gcocmd =~ /add_order_number/) {
1186        $xmlOut = <<EOX;
1187 <?xml version="1.0" encoding="UTF-8"?>
1188 <add-merchant-order-number xmlns="http://checkout.google.com/schema/2" google-order-number="$gco_order_number">
1189     <merchant-order-number>$mv_order_number</merchant-order-number>
1190 </add-merchant-order-number>
1191 EOX
1192
1193         my $return = sendxml($xmlOut);
1194         my $xml    = new XML::Simple();
1195         my $xmlin  = $xml->XMLin("$return");
1196            $::Session->{payment_result}{Terminal} = 'success' unless ($xmlin->{'error-message'});       
1197            $::Session->{errors}{GoogleCheckout} = $xmlin->{'error-message'} if ($xmlin->{'error-message'});
1198     }
1199
1200 #--- refund order ------------------------------------------------------------------------------
1201 elsif ($gcocmd =~ /refund/) {
1202        $xmlOut = <<EOX;
1203 <?xml version="1.0" encoding="UTF-8"?>
1204 <refund-order xmlns="http://checkout.google.com/schema/2" google-order-number="$gco_order_number">
1205     <amount currency="$currency">$amount</amount>
1206     <reason>$::Values->{reason}</reason>
1207 </refund-order>
1208 EOX
1209    
1210    my $return = sendxml($xmlOut);
1211    my $xml   = new XML::Simple();
1212    my $xmlin = $xml->XMLin("$return");
1213           $::Session->{payment_result}{Terminal} = 'success' unless ($xmlin->{'error-message'});        
1214           $::Session->{errors}{GoogleCheckout} = $xmlin->{'error-message'} if ($xmlin->{'error-message'});
1215     }
1216
1217 #--- cancel order -------------------------------------------------------------------------------
1218 elsif ($gcocmd =~ /cancel/) {
1219        $xmlOut = <<EOX;
1220 <?xml version="1.0" encoding="UTF-8"?>
1221 <cancel-order xmlns="http://checkout.google.com/schema/2" google-order-number="$gco_order_number">
1222     <reason>$reason</reason>
1223 </cancel-order>
1224 EOX
1225       
1226    my $return = sendxml($xmlOut);
1227    my $xml    = new XML::Simple();
1228    my $xmlin  = $xml->XMLin("$return");
1229           $::Session->{payment_result}{Terminal} = 'success' unless ($xmlin->{'error-message'});        
1230           $::Session->{errors}{GoogleCheckout} = $xmlin->{'error-message'} if ($xmlin->{'error-message'});
1231     }
1232
1233 #--- authorise order ----------------------------------------------------------------------------
1234 elsif ($gcocmd =~ /authorise/) {
1235        $xmlOut = <<EOX;
1236 <?xml version="1.0" encoding="UTF-8"?>
1237 <authorize-order xmlns="http://checkout.google.com/schema/2" google-order-number="$gco_order_number"/>      
1238 EOX
1239    
1240    my $return = sendxml($xmlOut);
1241    my $xml    = new XML::Simple();
1242    my $xmlin  = $xml->XMLin("$return");
1243           $::Session->{payment_result}{Terminal} = 'success' unless ($xmlin->{'error-message'});        
1244           $::Session->{errors}{GoogleCheckout} = $xmlin->{'error-message'} if ($xmlin->{'error-message'});
1245    
1246    }
1247
1248 #--- archive order ------------------------------------------------------------------------------
1249 elsif ($gcocmd =~ /archive/) {
1250        $xmlOut = <<EOX;
1251 <?xml version="1.0" encoding="UTF-8"?>
1252 <archive-order xmlns="http://checkout.google.com/schema/2" google-order-number="$gco_order_number" />
1253 EOX
1254    
1255    my $return = sendxml($xmlOut);
1256    my $xml    = new XML::Simple();
1257    my $xmlin  = $xml->XMLin("$return");
1258           $::Session->{payment_result}{Terminal} = 'success' unless ($xmlin->{'error-message'});        
1259           $::Session->{errors}{GoogleCheckout} = $xmlin->{'error-message'} if ($xmlin->{'error-message'});
1260       
1261     }
1262
1263 #--- add tracking data --------------------------------------------------------------------------
1264 elsif ($gcocmd =~ /add_tracking/) {
1265        $xmlOut = <<EOX;
1266 <?xml version="1.0" encoding="UTF-8"?>
1267 <add-tracking-data xmlns="http://checkout.google.com/schema/2" google-order-number="$gco_order_number">
1268     <tracking-data>
1269         <carrier>$carrier</carrier>
1270         <tracking-number>$tracking_number</tracking-number>
1271     </tracking-data>
1272 </add-tracking-data>
1273 EOX
1274      
1275    my $return = sendxml($xmlOut);
1276    my $xml    = new XML::Simple();
1277    my $xmlin  = $xml->XMLin("$return");
1278           $::Session->{payment_result}{Terminal} = 'success' unless ($xmlin->{'error-message'});        
1279           $::Session->{errors}{GoogleCheckout} = $xmlin->{'error-message'} if ($xmlin->{'error-message'});
1280          
1281     }
1282
1283 #--- deliver order --------------------------------------------------------------------
1284 elsif ($gcocmd =~ /deliver/) {
1285        $xmlOut = <<EOX;
1286 <?xml version="1.0" encoding="UTF-8"?>
1287 <deliver-order xmlns="http://checkout.google.com/schema/2" google-order-number="$gco_order_number">
1288     <tracking-data>
1289         <carrier>$carrier</carrier>
1290         <tracking-number>$tracking_number</tracking-number>
1291     </tracking-data>
1292     <send-email>$send_email</send-email>
1293 </deliver-order>
1294 EOX
1295    
1296    my $return = sendxml($xmlOut);
1297    my $xml    = new XML::Simple();
1298    my $xmlin  = $xml->XMLin("$return");
1299           $::Session->{payment_result}{Terminal} = 'success' unless ($xmlin->{'error-message'});        
1300           $::Session->{errors}{GoogleCheckout} = $xmlin->{'error-message'} if ($xmlin->{'error-message'});
1301        
1302        }
1303
1304 #--- end of admin panel commands ----------------------------------------------------------------------------
1305     
1306     }
1307  
1308 }
1309
1310 #------------------
1311 sub new {
1312   my (%args) = @_;
1313   my $class = 'Vend::Payment::GoogleCheckout';
1314   my $self;
1315      $self->{__merchant_id}        = $args{merchant_id};
1316      $self->{__merchant_key}       = $args{merchant_key};
1317      $self->{__base_gco_server}    = $args{gco_server};
1318      $self->{__currency_supported} = $args{currency_supported} || 'USD';
1319      $self->{__xml_schema}         = $args{xml_schema} || 'http://checkout.google.com/schema/2';
1320      $self->{__xml_version}        = $args{xml_version} || '1.0';
1321      $self->{__xml_encoding}       = $args{xml_encoding} || 'UTF-8';
1322 # ::logDebug(":GCO:".__LINE__.": class=$class; id=$self->{__merchant_id}; key=$self->{__merchant_key} server=$self->{__base_gco_server}");
1323   return bless $self => $class;
1324 }
1325
1326 sub sendxml {
1327 use MIME::Base64;
1328   my $xmlOut = shift;
1329   my $agent  = LWP::UserAgent->new;
1330   my $data   = "$merchantid:$merchantkey";
1331   my $signature = encode_base64($data, "");
1332      $header  = HTTP::Headers->new;
1333      $header->header('Authorization' => "Basic " . $signature);
1334      $header->header('Content-Type'  => "application/xml; charset=UTF-8");
1335      $header->header('Accept'        => "application/xml");
1336   my $request = HTTP::Request->new(POST => $gcourl, $header, $xmlOut);
1337   my $response = $agent->request($request);
1338 ::logDebug(":GCO:".__LINE__.": sendxml: gcourl=$gcourl\nxmlOut=$xmlOut");
1339   return $response->content;
1340 }
1341
1342 1;