1 # Vend::Payment::GoogleCheckout - Interchange Google Checkout support
3 # GoogleCheckout.pm, v 0.7.3, September 2009
5 # Copyright (C) 2009 Zolotek Resources Ltd. All rights reserved.
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.
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.
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,
24 package Vend::Payment::GoogleCheckout;
26 =head1 Interchange GoogleCheckout support
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.
33 Lyn St George <info@zolotek.net>
37 Steve Graham, mrlock.com, debugging and documentation
38 Andy Smith, tvcables.co.uk, debugging and documentation
43 XML::Simple and any of its prerequisites (eg XML::Parser or XML::SAX)
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.
49 Interchange.cfg should contain this line, with args extra to 'rand' for current UTF8 problems:
50 SafeUntrap rand require caller dofile print entereval
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.
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.
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).
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.
84 ##########################################################################################################
85 # While you can send & and have it returned safely, you cannot send <, > 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 < and similar UTF-8 entities, Google will return them in the < format. #
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 ##########################################################################################################
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.
101 Place this module in your Vend/Payment/ directory, and call it in interchange.cfg with:
102 Require module Vend::Payment::GoogleCheckout
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.
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.
133 Add these order routes to catalog.cfg
134 Route googlecheckout <<EOF
140 report etc/log_transaction
141 track logs/tracking.asc
142 counter_tid logs/tid.counter
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__
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.
155 Create a GoogleCheckout button on your checkout page, including the order profile and route like so:
158 text="GoogleCheckout"
162 mv_order_profile=googlecheckout
163 mv_order_route=googlecheckout
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
172 To have GoogleCheckout co-operate with your normal payment service provider, eg Authorizenet, do the
175 Add to etc/profiles.order:
177 __NAME__ googlecheckout
178 __COMMON_ORDER_PROFILE__
184 &set=mv_payment_route googlecheckout
185 &set=mv_order_route googlecheckout
187 &setcheck = payment_method googlecheckout
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.
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
198 Within the 'credit_card' section of etc/profiles.order leave
201 &set=psp __MV_PAYMENT_PSP__
202 &set=mv_payment_route authorizenet
203 (or your preferred gateway) as the last entries in the section.
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).
211 Run the following at a MySQL prompt to add the requisite fields to your transactions table:
212 (with thanks to Steve Graham)
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,
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);
242 In etc/log_transction, immediately after the
243 [elsif variable MV_PAYMENT_MODE]
246 undef $Session->{payment_result}{MStatus};
249 [elsif variable MV_PAYMENT_MODE]
250 as set (contrary to earlier revisions of this document), but within the same section change the following
252 [var MV_PAYMENT_MODE] to [value mv_payment_route]
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.
262 Still in etc/log_transaction, find the section that starts "Set order number in values: " and insert
264 [if value mv_order_profile =~ /googlecheckout/]
265 [value name=mv_order_number set="[scratch purchaseID]" scratch=1]
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.
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.
284 =item AVS and CV2 risk assessment:
285 avs_match_accepted partial|full|none
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)
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.
298 cv2_match_accepted yes|none
301 U - CVN not available
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'.
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.
310 =item Google Analytics
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.
317 =item Error messages from GCO
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.
325 [tmp uri][env REQUEST_URI][/tmp]
326 [if type=explicit compare=`$Scratch->{uri} =~ /%20400%20Bad%20Request%3C\?xml/`]
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;
331 [bounce href="[area ord/checkout]"]
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 £ instead of £)
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'
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.
358 - updated documentation, simplifed system for co-operating with other payment systems.
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.
369 package Vend::Payment;
372 require MIME::Base64;
373 require HTTP::Request::Common;
374 import HTTP::Request::Common qw(POST);
376 require HTML::Entities;
378 import Encode qw(encode decode);
379 require Data::Dumper;
380 $selected = "XML::Simple and MIME::Base64";
383 $Vend::Payment::Have_Google = 1 unless $@;
385 unless ($Vend::Payment::Have_Google) {
386 die __PACKAGE__ . " requires XML::Simple, MIME::Base64";
391 ::logGlobal("%s v0.7.3 payment module initialised, using %s", __PACKAGE__, $selected) unless $Vend::Quiet;
395 package Vend::Payment;
397 my ($gcourl,$merchantid,$merchantkey,$gcoserver,$xmlOut, $taxrate, $state, $header, $gcorequest, $actual, $orderID);
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
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} = '';
455 if ($gcorequest eq 'post') {
456 $gcourl = "$gcoserver/merchantCheckout/Merchant/$merchantid";
459 $gcourl = "$gcoserver/request/Merchant/$merchantid";
462 $gcourl .= "/diagnose" if ($diagnose == '1');
463 #::logDebug(":GCO:".__LINE__.": gcourl=$gcourl");
466 merchant_id => $merchantid,
467 merchant_key => $merchantkey,
468 gco_server => $gcourl,
469 currency_supported => $currency
472 my (%actual) = map_actual();
475 #::logDebug(":GCO:".__LINE__." actual map result: " . ::uneval($actual));
477 #----------------------------------------------------------------------------------------
478 # Initial post to GCO
479 #----------------------------------------------------------------------------------------
480 if ($gcorequest eq 'post') {
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') {
508 elsif ($taxshipping eq 'true') {
509 $taxrate = ($salestax / ($subtotal + $shipping) || '0');
511 elsif ($calculate_included_tax == '1') {
512 $taxrate = $default_taxrate;
515 $taxrate = ($salestax / $subtotal || '0');
517 ::logDebug(":GCO:".__LINE__.": subtotal=$subtotal; taxrate=$taxrate");
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;
528 #::logDebug(":GCO:".__LINE__.": ordertot=$ordertotal; subtot=$subtotal; amount=$::Values->{amount}; tax=$::Values->{tax} - $salestax; invno=$::Values->{inv_no}; zip=$zip_pattern; country=$country");
530 my ($item, $itm, $basket);
531 if (($::Values->{inv_no}) or ($::Values->{digital_delivery})) {
532 $shipmode = 'Digital';
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>
543 # TODO: allow for carts other than 'main'
544 elsif ($::Carts->{'main'}) {
545 foreach $item (@{$::Carts->{'main'}}) {
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)
555 # Trailing white space, raw & < > are all 'invalid xml'.
556 $itm->{code} =~ s/\s*$//g;
557 $itm->{code} =~ s/&/&/g;
558 $itm->{code} =~ s/</</g;
559 $itm->{code} =~ s/>/>/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;
568 if ($itm->{tax_category}) {
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>
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>
595 $msg = errmsg("You must pass something to GoogleCheckout");
596 $::Session->{errors}{GoogleCheckout} = $msg;
600 $orderID = gen_order_id($opt);
601 $::Scratch->{orderID} = $orderID;
602 $::Scratch->{txtype} = 'GCO - PENDING';
604 # Disable order number creation in log_transaction and create it here instead
605 if ($::Values->{inv_no}) {
606 $purchaseID = $::Values->{inv_no};
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};
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;");
618 $::Scratch->{purchaseID} = $purchaseID;
620 #::logDebug(":GCO:".__LINE__.": txtype=$::Scratch->{txtype}; orderid=$orderID, purchaseid=$purchaseID");
624 <?xml version="1.0" encoding="UTF-8"?>
625 <checkout-shopping-cart xmlns="http://checkout.google.com/schema/2">
627 <merchant-private-data>
628 <merchant-note>$purchaseID</merchant-note>
629 </merchant-private-data>
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>
646 <shipping-taxed>$taxshipping</shipping-taxed>
647 <rate>$taxrate</rate>
651 if ($country =~ /US/i) {
654 <state>$state</state>
661 <country-code>$country</country-code>
671 <alternate-tax-tables>
672 <alternate-tax-table standalone="true" name="$reduced_taxfield">
673 <alternate-tax-rules>
675 <rate>$reduced_taxrate</rate>
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>
688 <flat-rate-shipping name="$shipmode">
689 <price currency="$currency">$shipping</price>
690 <shipping-restrictions>
694 if ($country =~ /US/i) {
697 <state>$state</state>
700 <zip-pattern>$zip_pattern</zip-pattern>
707 <country-code>$country</country-code>
708 <postal-code-pattern>$zip_pattern</postal-code-pattern>
714 </shipping-restrictions>
715 </flat-rate-shipping>
717 <analytics-data>$analytics_data</analytics-data>
719 <parameterized-url url="$returnurl"/>
720 </parameterized-urls>
721 </merchant-checkout-flow-support>
722 </checkout-flow-support>
723 </checkout-shopping-cart>
727 # Write full order to orders/gco/ using gco$date.$session_id file name as failsafe backup in case order
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;
735 mkdir "$ordersdir/gco", 0775 unless -d "$ordersdir/gco";
737 my $fn = Vend::Util::catfile(
739 "gco$date.$::Session->{id}"
742 Vend::Util::writefile( $fn, $page )
743 or ::logError("GCO error writing failsafe order $fn: $!");
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'};
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);
762 $::Tag->tag({ op => 'header', body => <<EOB });
764 Location: $redirecturl
768 # Fake the result so that IC can log the transaction
769 $result{Status} = 'success';
770 $result{MStatus} = 'success';
771 $result{'order-id'} = $orderID;
778 #----------------------------------------------------------------------------------------
779 # Now handle callbacks, eg notification of payment, risk assessment, etc
780 #----------------------------------------------------------------------------------------
782 elsif ($gcorequest eq 'callback') {
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
788 my $authdata = $ENV{HTTP_AUTHORIZATION};
789 my ($id, $key, $authed, $email, $locale, $company_name, $new_order_no, $date, $phone, $sendermail);
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)) {
806 if (($authed eq 'yes') or ($bypass_auth == '1')) {
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'");
815 my $xml = new XML::Simple();
816 my $xmlin = $xml->XMLin("$$xmlIpn");
817 my $gco_serial_number = $xmlin->{'serial-number'};
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/);
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/);
874 $postal_code =~ /^(\S\S\S).*/;
875 my $postal_code_short = $1;
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'");
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'};
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];
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);
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");
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'");
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");
932 my ($mailout, $finstatus);
934 if ($gco_financial_state =~ /PAYMENT_DECLINED/i) {
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.
939 $finstatus = "declined by your bank";
941 elsif ($gco_financial_state =~ /CANCELLED/i) {
943 Google Order number $gco_order_number from $::Variable->{COMPANY} has been cancelled.
945 $finstatus = "cancelled";
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",
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'};
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'");
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)) {
977 elsif (($avsmatch) eq 'partial' and ($gco_avs_response !~ /N/i)) {
980 elsif ($avsmatch eq 'none') {
984 if (($cv2match) eq 'yes' and ($gco_cvn_response !~ /N/i)) {
987 elsif ($cv2match eq 'none') {
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();
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>
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"
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'};
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];
1062 my $username = $d[35];
1066 $cart = eval($cart);
1067 #::logDebug(":GCO:".__LINE__.": cart=$cart");
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;
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'");
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'");
1087 # Add IC order number to GCO admin panel
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>
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;
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;
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'};
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'");
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.
1122 $::Tag->email({ to => "$merchantemail", from => "$senderemail", reply => "$sendermail",
1123 subject => "Google order $gco_order_number has had a CHARGEBACK",
1124 body => "$mailchargeback\n",
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'};
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'");
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.
1145 $::Tag->email({ to => "$merchantemail", from => "$senderemail", reply => "$sendermail",
1146 subject => "Google order $gco_order_number has been refunded",
1147 body => "$mailrefund\n",
1153 #===================================================================================================
1154 # Now deal with any commands: charge card, ship, refund etc, which might come through an admin panel
1155 #---------------------------------------------------------------------------------------------------
1157 elsif ($gcorequest eq 'command') {
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};
1168 #--- charge order -----------------------------------------------------------------------------
1169 if ($gcocmd =~ /charge/) {
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>
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'});
1184 #--- add Interchange order number --------------------------------------------------------------
1185 elsif ($gcocmd =~ /add_order_number/) {
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>
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'});
1200 #--- refund order ------------------------------------------------------------------------------
1201 elsif ($gcocmd =~ /refund/) {
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>
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'});
1217 #--- cancel order -------------------------------------------------------------------------------
1218 elsif ($gcocmd =~ /cancel/) {
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>
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'});
1233 #--- authorise order ----------------------------------------------------------------------------
1234 elsif ($gcocmd =~ /authorise/) {
1236 <?xml version="1.0" encoding="UTF-8"?>
1237 <authorize-order xmlns="http://checkout.google.com/schema/2" google-order-number="$gco_order_number"/>
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'});
1248 #--- archive order ------------------------------------------------------------------------------
1249 elsif ($gcocmd =~ /archive/) {
1251 <?xml version="1.0" encoding="UTF-8"?>
1252 <archive-order xmlns="http://checkout.google.com/schema/2" google-order-number="$gco_order_number" />
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'});
1263 #--- add tracking data --------------------------------------------------------------------------
1264 elsif ($gcocmd =~ /add_tracking/) {
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">
1269 <carrier>$carrier</carrier>
1270 <tracking-number>$tracking_number</tracking-number>
1272 </add-tracking-data>
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'});
1283 #--- deliver order --------------------------------------------------------------------
1284 elsif ($gcocmd =~ /deliver/) {
1286 <?xml version="1.0" encoding="UTF-8"?>
1287 <deliver-order xmlns="http://checkout.google.com/schema/2" google-order-number="$gco_order_number">
1289 <carrier>$carrier</carrier>
1290 <tracking-number>$tracking_number</tracking-number>
1292 <send-email>$send_email</send-email>
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'});
1304 #--- end of admin panel commands ----------------------------------------------------------------------------
1313 my $class = 'Vend::Payment::GoogleCheckout';
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;
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;