Add payment module for MerchantWare 4.0 gateway, from Merchant
[interchange.git] / lib / Vend / Payment / Worldpay.pm
1 # Vend::Payment::Worldpay - Interchange Worldpay support
2 #
3 # worldpay.pm, v 1.0.2, October 2010
4 #
5 # Copyright (C) 2009 Nimbus Designs Ltd T/A TVCables and 
6 # Zolotek Resources Ltd All rights reserved.
7 #
8 # Authors: Andy Smith <andy@tvcables.co.uk>, http://www.tvcables.co.uk
9 # Lyn St George <info@zolotek.net, http://www.zolotek.net>
10 # Based on original code by Mike Heins <mheins@perusion.com> and others.
11 # This program is free software; you can redistribute it and/or modify
12 # it under the terms of the GNU General Public Licence as published by
13 # the Free Software Foundation; either version 2 of the Licence, or
14 # (at your option) any later version.
15 #
16 # This program is distributed in the hope that it will be useful,
17 # but WITHOUT ANY WARRANTY; without even the implied warranty of
18 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19 # GNU General Public Licence for more details.
20 #
21 # You should have received a copy of the GNU General Public
22 # Licence along with this program; if not, write to the Free
23 # Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
24 # MA  02111-1307  USA.
25
26
27
28 package Vend::Payment::Worldpay;
29
30 =head1 Interchange Worldpay Support
31
32 Vend::Payment::Worldpay $Revision: 1.0.2 $
33
34 http://kiwi.zolotek.net is the home page with the latest version.
35
36 =head1 This package is for the 'Worldpay' payment system.
37
38
39 =head1 Quick Start Summary
40
41 1 Place this module in <IC_root>/lib/Vend/Payment/Worldpay.pm
42
43 2 Call it in interchange.cfg with:
44     Require module Vend::Payment::Worldpay.
45     
46 3 Add a new route into catalog.cfg (options for the last entry in parentheses):
47   Route worldpay host https://secure.wp3.rbsworldpay.com/wcc/purchase (Live Payment URL)
48   Route worldpay testhost https://secure-test.wp3.rbsworldpay.com/wcc/dispatcher (Test payment URL)
49   Route worldpay instid 12345 (Your Worldpay instID)
50   Route worldpay currency GBP (defaults to GBP)
51   Route worldpay testmode 100 (Set to 100 for test mode 0 for live - default live)
52   Route worldpay callbackurl (The URL Worldpay will callback eg www.yourstore.co.uk/cgi-bin/yourname/wpcallback.html)
53   Route worldpay callpw (Callback password, set any password you like and set it the same in the WP Admin Panel)
54   Route worldpay fixcontact 1 (If set to 1 customers cannot ammend address details when they get to worldpay, 0 to allow changes)
55   Route worldpay desc 'Yourstore Order' (Text to send in the desc field eg 'Yourstore Order')
56   Route worldpay reporttitle 1 (If set to 1 will modifty order report title to include transaction ID)
57   Route worldpay update_status processing (Text to set order status on success eg processing, default pending)
58   Route worldpay wpcounter (Defines the counter for temporary order number, defaults to etc/username)
59   Route worldpay md5pw yourmd5secret (required in this version, will die without it)
60
61 ## md5 code ##
62   Enter the following into the worldpay control panel:-
63   Your MD5 secret as set in catalog.cfg
64   In the signature fields box enter amount:instId:MC_affsubtotal:currency
65
66 4 Create a new locale setting for en_GB as noted in "item currency" below, and copy the
67 public space interchange/en_US/ directory to a new interchange/en_GB/ one. Ensure that any
68 other locales you might use have a correctly named directory as well. Ensure that this locale
69 is found in your version of locale.txt (and set up GB as opposed to US language strings to taste).
70
71 5 Add a new order profile in etc/profiles.order
72
73 __NAME__                            worldpay
74 fname=required
75 b_fname=required
76 lname=required
77 b_lname=required
78 address1=required
79 b_address1=required
80 city=required
81 b_city=required
82 state=required
83 b_state=required
84 zip=required
85 b_zip=required
86 &fatal = yes
87 email=required
88 email=email
89 &set=mv_payment worldpay
90 &set=psp worldpay
91 &set=mv_payment_route worldpay
92 &set=mv_order_route worldpay
93 &final = yes
94 &setcheck = payment_method worldpay
95 __END__
96
97 6 Add the following fields to the transactions table if they do not already exist (run from a mysql prompt)
98
99 ALTER TABLE `transactions` ADD `wp_transtime` VARCHAR( 64 ) CHARACTER SET utf8 COLLATE utf8_general_ci ,
100 ADD `wp_cardtype` VARCHAR( 64 ) CHARACTER SET utf8 COLLATE utf8_general_ci,
101 ADD `wp_countrymatch` VARCHAR( 64 ) CHARACTER SET utf8 COLLATE utf8_general_ci,
102 ADD `wp_avs` VARCHAR( 64 ) CHARACTER SET utf8 COLLATE utf8_general_ci,
103 ADD `wp_risk` VARCHAR( 64 ) CHARACTER SET utf8 COLLATE utf8_general_ci,
104 ADD `wp_authentication` VARCHAR( 64 ) CHARACTER SET utf8 COLLATE utf8_general_ci,
105 ADD `wp_authamount` VARCHAR( 64 ) CHARACTER SET utf8 COLLATE utf8_general_ci,
106 ADD `wp_order_number` VARCHAR( 64 ) CHARACTER SET utf8 COLLATE utf8_general_ci,
107 ADD `wp_transtime` VARCHAR( 64 ) CHARACTER SET utf8 COLLATE utf8_general_ci,
108 ADD `lead_source` VARCHAR(255) CHARACTER SET utf8 COLLATE utf8_general_ci,
109 ADD `referring_url` VARCHAR(255) CHARACTER SET utf8 COLLATE utf8_general_ci,
110 ADD `txtype` VARCHAR(64) CHARACTER SET utf8 COLLATE utf8_general_ci,
111 ADD `email_copy` CHAR(1) CHARACTER SET utf8 COLLATE utf8_general_ci,
112 ADD `mail_list` varCHAR(255) CHARACTER SET utf8 COLLATE utf8_general_ci,
113 ADD `cartid` VARCHAR(64) CHARACTER SET utf8 COLLATE utf8_general_ci,
114 ADD `cart` BLOB;
115
116
117 And run these to allow for temporary order numbers of greater than the default 14 character field type
118 ALTER TABLE `transactions` MODIFY `order_number` varchar(32);
119 ALTER TABLE `orderline` MODIFY `order_number` varchar(32);
120
121 Also add 'cartid' to the orderline table.
122
123 7. Add the following to etc/log_transaction just BEFORE [/import][/try]
124
125 lead_source: [data session source]
126 referring_url: [data session referer]
127 cart: [calc]uneval($Items)[/calc]
128 cartid: [value mv_order_number]
129 email_copy: [value email_copy]
130 mail_list: [value mail_list]
131
132 8. Still in etc/log_transaction, find the section that starts "Set order number in values: " and insert
133 this just before it:
134
135 NB/ this only applies if your IC is greater than v 5.2, otherwise skip sections 8 & 9
136
137 [if value mv_order_profile =~ /worldpay/]
138 [value name=mv_order_number set="[scratch purchaseID]" scratch=1]
139 [else]
140 and a closing [/else][/if] at the end of that section, just before the 
141 "Set order number in session:"
142 line. The order number is generated by the module and passed to Worldpay at an early stage, and then
143 passed back to Interchange with a callback. This prevents Interchange generating another order number.
144 The module will not currently work with IC versions lower than 5.2 that use a tid counter defined in
145 catalog.cfg. The initial order number uses the username.counter number prefixed with 'WPtmp', and a normal
146 order number is created and the initial order number replaced only when Worldpay calls back that the card
147 has been charged. This is to avoid gaps in the order number sequence caused by customers abandoning the 
148 transaction.
149
150 9. In etc/log_transction, change the line:- 
151 [elsif variable MV_PAYMENT_MODE]
152 to
153 [elsif value mv_order_profile =~ /worldpay/] add an OR if required
154 eg [elsif value mv_order_profile =~ /googlecheckout|worldpay/]
155
156 Then in the [calc] block immediately below insert this line: 
157
158         undef $Session->{payment_result}{MStatus};
159         
160 Within the same section change the following two instances of
161 [var MV_PAYMENT_MODE] to [value mv_payment_route]
162
163 10. Create a callback page in /pages called wpcallback.html or any name you prefer, set this page in
164 the Worldpay admin panel, the module also supports dynamic callback pages where different catalogs can
165 have different callback pages, if using this the callpage URL must be set in the route in catalog.cfg as
166 described above.
167
168 At the top of the callback page include the following line:-
169 [charge route="worldpay" worldpayrequest="callback"]
170
171 At the end of the charge process Worldpay do not allow redirection to a receipt page, if you do this they
172 claim they will disable the callback feature or even suspend your account, how nice! You can however re-direct if
173 the transaction is cancelled
174
175 Worldpay will suck the wpcallback page back to their server and display it for you, this can be used to display a receipt page.
176 The page will interpolate before being sucked to Worldpay so most items such as fname lname adress fields etc are usuable on the page.
177 To display banners and logos they need to be pre-loaded onto the Worldpay server
178
179 At the top of the callback page just below the [charge route="worldpay" worldpayrequest="callback"] you can test for a successful transaction as follows:-
180
181 [if type="cgi" term="transStatus" op="eq" compare="Y"] 
182 [and type="cgi" term="callbackPW" op="eq" compare="yourcallbackpassword"] 
183
184 Display a receipt page
185
186 [else]
187
188 Display a cancelled page or bounce the customer back to site etc
189
190 [/else]
191 [/if]
192
193 11. Checkout button
194
195 On your checkout page include a button that sets the route and submits the checkout form eg
196
197 [button
198     mv_click=worldpay
199     text="Place Order"
200     hidetext=1
201     form=checkout
202    ]
203    mv_order_profile=worldpay
204    mv_order_route=log
205    mv_todo=submit
206 [/button]
207
208 NB/ for IC versions 5.2 and older, make the button so as to call the 'wprequest.html' page:
209
210 [button
211     mv_click=worldpay
212     text="Place Order"
213     hidetext=1
214     form=checkout
215    ]
216    mv_nextpage=ord/wprequest
217    mv_order_route=log
218    mv_todo=submit
219 [/button]
220
221
222 =head1 PREREQUISITES
223
224   Net::SSLeay
225     or
226   LWP::UserAgent and Crypt::SSLeay
227
228   wget - a recent version built with SSL and supporting the 'connect' timeout function.
229
230
231 =head1 DESCRIPTION
232
233 The Vend::Payment::Worldpay module implements the Worldpay() routine for use with
234 Interchange. It is _not_ compatible on a call level with the other Interchange
235 payment modules.
236
237 To enable this module, place this directive in <interchange.cfg>:
238
239     Require module Vend::Payment::Worldpay
240
241 This I<must> be in interchange.cfg or a file included from it.
242
243 The module collects the data from a checkout form and formats it with a re-direct to
244 the Worldpay payment server. The customers details and cart is logged in the database
245 before going to Worldpay with a temporary order number of the form WPtmpUxxxx where Uxxxx
246 is derived from the username counter
247
248 If the transaction is successful the module processes the callback response from Worlday, if
249 successful the temporary order number is converted to an Interchange order number and a final
250 route is run to send out the report and customer emails. Cancelled transactions remain in the
251 database with the temporary order numbers but are automatically archived.
252
253 The module will also optionally decrement the inventory on a successful, if used
254 the inventory decrement in log transaction should be disabled by setting the appropriate variable
255
256 =head1 The active settings.
257
258 The module uses several of the standard settings from the Interchange payment routes.
259 Any such setting, as a general rule, is obtained first from the tag/call options on
260 a page, then from an Interchange order Route named for the mode in catalog.cfg,
261 then a default global payment variable in products/variable.txt, and finally in
262 some cases a default will be hard-coded into the module.
263
264 =over
265
266 =item instid
267
268 Your installation id supplied by Worldpay, the module cannot be used without an instid, set in
269 catalog.cfg
270
271 =item currency
272
273 Worldpay requires that a currency code be sent, using the 3 letter ISO currency code standard,
274 eg, GBP, EUR, USD. The value is taken firstly from the route parameter in catalog.cfg and
275 defaults to GBP
276
277 =item testmode
278
279 Sets whether the system runs test or live transactions, set to 0 (default) for live transactions, 
280 or 100 for test transactions.
281
282 =item callbackurl
283
284 If using dynamic callback pages with Worldpay, set you callback page without the http eg:-
285
286 www.yourstore.co.uk/cgi-bin/yourstore/wpcallback.html
287
288 =item callpw
289
290 Sets the password to compare with the callback, set this the same as the password in the Worldpay
291 admin panel
292
293
294 =item desc
295
296 Sets the text for the desc field sent to Worldpay and will appear on the transaction reciper, eg
297 'Yourstore Order'
298
299 =item fixcontact
300
301 Fixes the information send to Worldpay so it cannot be modified by the customer at Worldpay, set to 1
302 to fix or 0 to allow the customer to edit address at Worldpay.
303
304 =item reporttitle
305
306 Set to 1 to change the email report title to include the Worldpay transaction ID, set to zero for
307 standard report email title
308
309 =item update_status
310
311 Allows the order status to be set to any desired value after a successful transaction, eg set to processing
312 and all successfull transactions will have status processing, defaults to pending
313
314 =item dec_inventory
315
316 Set to 1 for module to decrement the inventory on a successful transaction, if used disable decrement via
317 log_transaction.
318
319
320 =back
321
322 =head2 Testing
323
324 Set testmode 100 in catalog.cfg
325
326 Add some items to the cart and place the order, the module will re-direct you
327 to Worldpay where you can select the card type to pay with. Enter some test card details and check
328 the order is logged in the database ok and emails sent out.
329
330 Test card numbers
331
332 Mastercard
333 5100080000000000
334
335 Visa Delta - UK
336 4406080400000000
337
338 Visa Delta - Non UK
339 4462030000000000
340
341 Visa
342 4911830000000
343
344 Visa
345 4917610000000000
346
347 American Express
348 370000200000000
349
350 Diners
351 36700102000000
352
353 JCB
354 3528000700000000
355
356 Visa Electron (UK only)
357 4917300800000000
358
359 Solo
360 6334580500000000
361
362 Solo
363 633473060000000000
364
365 Discover Card
366 6011000400000000
367
368 Laser
369 630495060000000000
370
371 Maestro
372 6759649826438453
373
374 Visa Purchasing
375 4484070000000000
376
377
378 =head1 changelog
379  - v1.0.2 November 2011, added encryption from Andy to main request as defence against tampering
380  - v1.0.1 not released: October 2010, made work with IC v4.8.7 - needs to have 'use strict' commented out, and a redirection page
381   
382
383 =head1 AUTHORS
384
385 Andy Smith <andy@tvcables.co.uk> with help from and based on code by
386 Lyn St George <lyn@zolotek.net>, which in turn was based on original
387 code by Mike Heins <mike@perusion.com> and others.
388
389 =cut
390
391 BEGIN {
392
393     my $selected;
394     eval {
395         package Vend::Payment;
396         require Net::SSLeay;
397         import Net::SSLeay qw(post_https make_form make_headers);
398         $selected = "Net::SSLeay";
399     };
400
401     $Vend::Payment::Have_Net_SSLeay = 1 unless $@;
402
403     unless ($Vend::Payment::Have_Net_SSLeay) {
404
405         eval {
406             package Vend::Payment;
407             require LWP::UserAgent;
408             require HTTP::Request::Common;
409             require Crypt::SSLeay;
410             require CGI;
411             use Digest::MD5 qw(md5_hex);
412             require Encode;
413              import Encode qw(encode decode);
414              import HTTP::Request::Common qw(POST);
415             $selected = "LWP and Crypt::SSLeay";
416         };
417
418         $Vend::Payment::Have_LWP = 1 unless $@;
419     }
420
421     unless ($Vend::Payment::Have_Net_SSLeay or $Vend::Payment::Have_LWP) {
422         die __PACKAGE__ . " requires Net::SSLeay or Crypt::SSLeay, " . $@;
423     }
424
425 ::logGlobal("%s 1.0.2 payment module initialised, using %s", __PACKAGE__, $selected)
426         unless $Vend::Quiet;
427
428 }
429
430 package Vend::Payment;
431 use strict; 
432
433 sub worldpay {
434         my ($amount, $actual, $opt, $worldpayrequest, $cart, $orderID, $purchaseID, %result, $dbh, $sql, $sth, $stho);
435
436 #::logDebug("WP".__LINE__.": amnt=$amount, req=$worldpayrequest, pID=$purchaseID");     
437         # Amount to send with 2 decimals and no symbol
438            $amount =  $::Values->{amount} || charge_param('amount') || Vend::Interpolate::total_cost();
439            $amount =~ s/^\D*//g;
440            $amount =~ s/\,//g;
441            $amount =  sprintf '%.2f', $amount;
442            
443         my $oldic  = charge_param('oldic');   
444  
445         # Transaction variables to send to worldpay.
446         my $host = charge_param('host') || 'https://secure.wp3.rbsworldpay.com/wcc/purchase'; #Live 
447         my $testhost = charge_param('testhost') || 'https://secure-test.wp3.rbsworldpay.com/wcc/purchase';#Test 
448         my $instId = charge_param('instid');
449         my $accId1 = charge_param('accid1');
450         my $currency = charge_param('currency') || 'GBP';
451     my $charged = charge_param('authtype') ||  'WP PreAuthed';
452         my $testMode = charge_param('testmode') || '0';
453         my $authMode = $::Values->{'authmode'} || charge_param('authmode') || 'E';
454         my $callbackurl = charge_param('callbackurl');  #URL on your server WP will callback, including .html extension
455         my $callpw = charge_param('callpw'); #Must be same as Worldpay admin panel callback password
456         my $desc;       #Transaction description, set to CartID for easier reference
457         my $tmpPrefix = charge_param('tmporderprefix') || 'Cart';
458         my $fixcontact = charge_param('fixcontact') || '0';    #0=details editable at WP 1=details fixed as sent 
459         my $affsubtotal = Vend::Interpolate::subtotal(); # This is used to send subtotal as as MC_ parameter to read back for affilate sales calculations
460            $affsubtotal =~ s/^\D*//g;
461            $affsubtotal =~ s/\,//g;
462            $affsubtotal =  sprintf '%.2f', $affsubtotal;
463         
464 #::logDebug("WP:".__LINE__.": Session = $Vend::Session->{id} Host = $host instid = $instId currency = $currency testmode = $testMode callbackurl = $callbackurl pw = $callpw desc = $desc fix = $fixcontact  affsubtotal=$affsubtotal");
465
466            $host = $testhost if ($testMode > '0');# send to test url not live
467
468         my $ordernumber  = charge_param('ordernumber') || 'etc/order.number';
469         my $wpcounter   = charge_param('wpcounter') || 'etc/username.counter'; 
470         my $username    = $Vend::Session->{'username'};
471         my $allowbilling = charge_param('allow_billing') || '';
472         my $md5pw = charge_param('md5pw') or die "No MD5 password set in route";
473
474            $worldpayrequest  = charge_param('worldpayrequest')  || $::Values->{worldpayrequest} || 'post';
475            $opt     = {};
476
477   
478 #::logDebug("WP:".__LINE__.": Request = $worldpayrequest; un=$username, $::Values->{mv_username},");
479            $::Values->{'mv_order_number'} = '';
480         
481 ##-----------Post Information and send customer to Worldpay------------##
482 if ($worldpayrequest eq 'post') {       
483 #::logDebug("WP:".__LINE__.": Sending customer to Worldpay");
484 #::logDebug("WP:".__LINE__.": TestMode = $testMode : Host = $host");
485
486         my $separator = '&#10;';
487         my $name = "$::Values->{fname} $::Values->{lname}" || "$::Values->{b_fname} $::Values->{b_lname}";
488         my $address = "$::Values->{address1}%0A$::Values->{address2}%0A$::Values->{city}%0A$::Values->{state}" || "$::Values->{b_address1}%0A$::Values->{b_address2}%0A$::Values->{b_city}%0A$::Values->{b_state}"; #%0A is the line feed separator between address lines
489         my $postcode = "$::Values->{zip}" || $::Values->{b_zip};
490         my $country = "$::Values->{country}" || "$::Values->{b_country}";
491         my $email = "$::Values->{email}";
492         my $tel = "$::Values->{phone_night}" || "$::Values->{phone_day}"; #some may wish to use phone_day
493
494            $orderID = gen_order_id($opt);
495            $::Scratch->{orderID} = $orderID;
496
497 # Disable order number creation in log_transaction and create it here instead, unless IC is old
498         if ($::Values->{inv_no}) {
499           $purchaseID = $::Values->{inv_no};
500                   }
501         else{
502 # Use temporary number as the initial order number, and only replace upon successful order completion
503                 $purchaseID = "$tmpPrefix".Vend::Interpolate::tag_counter("$wpcounter");
504                 $Vend::Session->{mv_order_number} = $::Values->{mv_order_number} = $purchaseID if ($oldic == 1);# prevents early ICs setting order number prior to log_transaction
505         ::logDebug("WP:".__LINE__.": purchaseID=$purchaseID; $Vend::Session->{mv_order_number}");
506         }
507     
508     my $cartId = $desc = $::Scratch->{purchaseID} = $purchaseID;
509
510         my $md5data = $md5pw . ":" . $amount . ":" . $instId . ":" . $affsubtotal . ":" . $currency;
511         my $signature = md5_hex($md5data);  
512 #::logDebug("WP:".__LINE__.": md5pw = $md5pw md5data = $md5data signature =$signature");
513
514 #go to worldpay
515   my $redirecturl = "$host?signature=$signature&instId=$instId&currency=$currency&testMode=$testMode&authMode=$authMode&amount=$amount&cartId=$cartId&desc=$desc&name=$name&address=$address";
516          $redirecturl .= "&postcode=$postcode&country=$country&email=$email&tel=$tel&MC_mv_order_number=$cartId&MC_callback=$callbackurl";
517          $redirecturl .= "&fixContact" if ($fixcontact == 1);
518          $redirecturl .= "&accId1=$accId1" if $accId1;
519          $redirecturl =~ s/(?:%0[da]|[\r\n]+)+//ig; ## "HTTP Response Splitting" Exploit Fix
520
521          $::Scratch->{'redirecturl'} = $redirecturl; # for old versions of IC needing a redirection page
522 #::logDebug("WP:".__LINE__.": redirectURL = $redirecturl");
523
524 # Fake the result so that IC can log the transaction
525         $result{Status}     = 'success';
526         $result{MStatus}    = 'success';
527         $result{'order-id'} = $orderID;
528 #::logDebug("WP".__LINE__.": resSt=$result{'Status'}; resMSt=$result{'MStatus'},resoid=$result{'order-id'}");
529
530 # Delete any stale baskets, ie with tmpID but without proper order numbers; only works if user is forced
531 # to login prior to ordering and uses same username
532     $dbh = dbconnectwp() or warn "###dbh failed\n";
533         $sql = "DELETE FROM transactions WHERE order_number LIKE '$tmpPrefix%' AND username='$username'";
534         $sth = $dbh->prepare("$sql") or warn "###sth failed\n";
535         $sth->execute() or die errmsg("###Transactions tbl failed") if $username;
536         $sql = "DELETE FROM orderline WHERE order_number LIKE '$tmpPrefix%' AND username='$username'";
537         $sth = $dbh->prepare("$sql") or warn "###sth failed\n";
538         $sth->execute() or die errmsg("###Transactions tbl failed") if $username;
539
540 $::Tag->tag({ op => 'header', body => <<EOB });
541 Status: 302 moved
542 Location: $redirecturl
543 EOB
544            return %result;
545 }
546
547 ####----------------Handle the callback from Worldpay--------------####
548 elsif ($worldpayrequest eq 'callback'){
549
550         my $newsess = $Vend::Session->{id};     
551 #::logDebug("WP:".__LINE__.": Processing Callback Session = $newsess");
552
553         my $reporttitle   = charge_param('reporttitle') || '';
554         my $update_status = charge_param('update_status') || 'pending';
555         my $dec_inventory = charge_param('dec_inventory') || '';
556
557 #Capture all callback fields 
558         my ($transid, $check_testmode, $transstatus, $authamount, $transtime, $authcurrency, $rawauthcode, $rawauthmessage, $callbackpw,        $cardtype, $countrymatch, $avs, $wafmerchmessage, $authentication, $ipaddress, $wp_order_number);
559         my $page = ::http()->{'entity'};
560 #::logDebug("WP".__LINE__.": page=\n$$page\n----------------------------------\n");
561         foreach my $line (split /\&/, $$page) {
562           $transid                  = $1 if ($line =~ /transId=(.*)/i);                 #transaction ID
563           $wp_order_number  = $1 if ($line =~ /MC_mv_order_number=(.*)/);# temp CartID sent
564           $check_testmode       = $1 if ($line =~ /testMode=(.*)/);                     #returns testmode value 0 live anything higher test mode
565           $transstatus          = $1 if ($line =~ /transStatus=(.*)/);          #Y=Sucess C=Cancelled
566           $authamount           = $1 if ($line =~ /authAmount=(.*)/);           #Authorised amount
567           $transtime            = $1 if ($line =~ /transTime=(.*)/);            #Time of transaction
568           $authcurrency         = $1 if ($line =~ /authCurrency=(.*)/);         #Currency of authorisation
569           $callbackpw           = $1 if ($line =~ /callbackPW=(.*)/);           #Callback password as set in admin panel
570           $rawauthmessage       = $::Values->{'rawauthmessage'}  = $1 if ($line =~ /rawAuthMessage=(.*)/);      #Raw auth message
571           $rawauthcode          = $::Values->{'rawauthcode'}     = $1 if ($line =~ /rawAuthCode=(.*)/); #Raw auth message
572           $cardtype                     = $::Values->{'cardtype'}        = $1 if ($line =~ /cardType=(.*)/);    #Card type used
573           $countrymatch         = $::Values->{'countrymatch'}    = $1 if ($line =~ /countryMatch=(.*)/); #Y=Match N=No match B=Not available I=Country not supplied S=Country issue not available
574           $avs                      = $::Values->{'avs'}             = $1 if ($line =~ /AVS=(.*)/);                     #AVS Results
575           $wafmerchmessage      = $::Values->{'wafmerchmessage'} = $1 if ($line =~ /wafMerchMessage=(.*)/);     #Risk result
576           $authentication       = $::Values->{'authentication'}  = $1 if ($line =~ /authentication=(.*)/);      #VbyV or Mastercard Securecode authentication type
577           $ipaddress            = $::Values->{'ipaddress'}       = $1 if ($line =~ /ipAddress=(.*)/);           #Shopper IP address
578    }
579           $::Values->{'cardtype'} = $cardtype =~ s/\+/ /g;
580           
581 #::logDebug("WP:".__LINE__.": transid=$transid testmode=$check_testmode transstatus=$transstatus authamount=$authamount transtime=$transtime authcurrency=$authcurrency rawauthmessage=$rawauthmessage");
582 #::logDebug("WP:".__LINE__.": callbackpw=$callbackpw cardtype=$cardtype countrymatch=$countrymatch avs=$avs wafmerchmessage=$wafmerchmessage authentication=$authentication ipaddress=$ipaddress");
583         
584         my $db  = Vend::Data::dbref('transactions') or die errmsg("cannot open transactions table");
585            $dbh = $db->dbh() or die errmsg("cannot get handle for tbl 'transactions'");
586
587 #::logDebug("WP:".__LINE__.": Callback order number = $wp_order_number");
588
589 #if success
590   if (($transstatus eq 'Y') and ($callbackpw eq $callpw)) { 
591 #::logDebug("WP:".__LINE__.": Transaction Successful");
592
593            $sth = $dbh->prepare("SELECT total_cost,email,txtype,order_number FROM transactions WHERE order_number='$wp_order_number'") or die errmsg("Cannot select from transactions tbl for $wp_order_number");
594            $sth->execute() or die errmsg("Cannot get data from transactions tbl");
595     my @d = $sth->fetchrow_array;
596     my $order_total = $d[0];
597     my $email       = $d[1];
598     my $txtype      = $d[2];
599     my $old_tid     = $d[3];
600 #generate the IC Order Number, and put in session to block old ICs from generating a second number
601         my $new_order_no = $::Values->{mv_order_number} = $Vend::Session->{mv_order_number} = Vend::Interpolate::tag_counter("$ordernumber"); 
602 #Check if transaction was in test mode
603     if ($check_testmode > '0') { # Transaction was in test mode
604                 $update_status = $update_status .'-TEST'; #Append Test to end of order status to show order was made in test mode
605                 $charged = $charged .'-TEST'; #Variable we write to txtype
606                 }    
607 #::logDebug("WP:".__LINE__.": new on = $new_order_no;  Check testmode = $check_testmode Update Status = $update_status Set txtype = $charged");
608                 
609 #Replace temporary order number with IC order number
610 #::logDebug("WP:".__LINE__.": Replacing order number: Old TID = $old_tid with New Order No = $new_order_no");
611            $sth = $dbh->prepare("UPDATE transactions SET code='$new_order_no', order_number='$new_order_no', txtype='$charged', payment_method='Worldpay ($cardtype)', cartid='$wp_order_number'  WHERE order_number='$wp_order_number'");
612            $sth->execute() or die errmsg("Cannot update transactions tbl for WP '$wp_order_number'");
613            $stho = $dbh->prepare("UPDATE orderline SET code=replace(code, '$old_tid', '$new_order_no'), order_number='$new_order_no' WHERE order_number='$old_tid'");
614            $stho->execute() or die errmsg("Cannot update transactions tbl for WP '$wp_order_number'");
615            
616 #Log transaction information & change order status
617 #::logDebug("WP:".__LINE__.": Logging transaction details to tbl for order $new_order_no");
618            $sth = $dbh->prepare("UPDATE transactions SET status='$update_status', order_id='$transid', wp_transtime='$transtime', wp_cardtype='$cardtype', wp_countrymatch='$countrymatch', wp_avs='$avs', wp_risk='$wafmerchmessage', wp_authentication='$authentication', wp_authamount='$authamount' WHERE order_number='$new_order_no'");
619            $sth->execute() or die errmsg("Cannot update transactions tbl for worldpay order '$new_order_no'");
620            $stho = $dbh->prepare("UPDATE orderline SET status='$update_status' WHERE order_number='$new_order_no'");
621            $stho->execute() or die errmsg("Cannot update orderline tbl for worldpay order '$new_order_no'");
622         
623 #Read the order details and cart from the database
624        $sth = $dbh->prepare("SELECT total_cost,email,order_number,fname,lname,company,address1,address2,city,state,zip,country,phone_day,fax,b_fname,b_lname,b_company,b_address1,b_address2,b_city,b_state,b_zip,b_country,shipmode,handling,order_date,lead_source,referring_url,txtype,locale,currency_locale,cart,session,salestax,shipping,b_phone,subtotal,cartid,email_copy,mail_list,free_sample FROM transactions WHERE order_number='$new_order_no'") or die errmsg("Cannot select from transactions tbl for $wp_order_number");
625        $sth->execute() or die errmsg("Cannot get data from transactions tbl");
626     my @d = $sth->fetchrow_array;
627     my $order_total = $::Values->{order_total} = $d[0];
628     my $email = $::Values->{email} = $d[1];
629     my $mv_order_number = $::Values->{mv_order_number} = $d[2];
630     my $fname = $::Values->{fname} = $d[3];
631     my $lname = $::Values->{lname} = $d[4];
632     my $company = $::Values->{company} = $d[5];
633     my $address1 = $::Values->{address1} = $d[6];
634     my $address2 = $::Values->{address2} = $d[7];
635     my $city = $::Values->{city} = $d[8];
636     my $state = $::Values->{state} = $d[9];
637     my $zip = $::Values->{zip} = $d[10];
638     my $country = $::Values->{country} = $d[11];
639     my $phone_day = $::Values->{phone_day} = $d[12];
640     my $fax = $::Values->{fax} = $d[13];
641     my $b_fname = $::Values->{b_fname} = $d[14];
642     my $b_lname = $::Values->{b_lname} = $d[15];
643     my $b_company = $::Values->{b_company} = $d[16];
644     my $b_address1 = $::Values->{b_address1} = $d[17];
645     my $b_address2 = $::Values->{b_address2} = $d[18];
646     my $b_city = $::Values->{b_city} = $d[19];
647     my $b_state = $::Values->{b_state} = $d[20];
648     my $b_zip = $::Values->{b_zip} = $d[21];
649     my $b_country = $::Values->{b_country} = $d[22];
650     my $shipmode = $::Values->{mv_shipmode} = $d[23];
651     my $handling = $::Values->{mv_handling} = $d[24];
652     my $order_date = $::Values->{order_date} = $d[25];
653     my $lead_source = $::Values->{lead_source} = $d[26];
654     my $referring_url = $::Values->{referring_url} = $d[27];
655     my $txtype = $::Values->{txtype} = $d[28];
656     my $mv_locale = $d[29];
657     my $mv_currency = $d[30] || 'GBP';
658     my $cart = $d[31];
659     my $session = $d[32];
660     my $salestax = $d[33] || '0';
661     my $shipping = $d[34] || '0';
662     my $phone_night = $::Values->{phone_night} = $d[35];
663         my $subtotal = $d[36] || '0';
664         my $cartID = $::Values->{'cartid'} = $d[37];
665     my $email_copy = $::Values->{'email_copy'} = $d[38];
666     my $mail_list = $::Values->{'mail_list'} = $d[39];
667         my $free_sample = $::Values->{'free_sample'} = $d[40];
668     
669     #todo add evening phone
670            $::Values->{'mv_shipmode'} ||= 'Standard';
671            $::Values->{mv_handling} = 1 if ($handling > '0');
672            $cartID = $wp_order_number unless defined $cartID;
673            Vend::Interpolate::tag_assign({ subtotal => "$subtotal", shipping => "$shipping", salestax => "$salestax" });
674            Vend::Interpolate::tag_assign({ handling => "$handling" }) if ($handling > '0');
675     
676         my (@cart, $acart);
677        $cart =~ s/\"/\'/g;
678        $cart =~ s/\\//;
679            @cart = eval($cart); 
680            $acart = eval ($cart);
681 #::logDebug("WP:".__LINE__.": cart=$cart Email=$email");        
682            
683            $::Values->{mv_payment} = 'Worldpay'." $::Values->{'cardtype'}";
684            $::Values->{wp_order_number} = $wp_order_number;
685            $::Session->{values}->{iso_currency_code} = $currency;
686            $::Session->{scratch}->{mv_locale} = $mv_locale;
687            $::Session->{scratch}->{mv_currency} = $mv_currency;
688            $::CGI::values{'mv_todo'} = 'submit';
689            $result{'MStatus'} = $result{'pop.status'} = 'success';
690            $result{'order-id'} = $orderID;
691            $result{'Status'} = 'OK';
692            $result{'WPStatus'} = 'success';
693            $Vend::Session->{'payment_result'} = \%result;
694
695  
696 #::logDebug("WP:".__LINE__.": Shipmode = $shipmode Shipping = $shipping Tax = $salestax Handling = $handling");
697
698
699 #Set new report title with final order number and WP transaction ID
700         if ($reporttitle == '1') {
701                 my $amt = sprintf '%.2f', $order_total;
702                 $::Values->{mv_order_subject} = 'Order '.$new_order_no.' : CartID '.$cartID.' : WPID:'.$transid.' : '.$mv_currency.''.$amt.' : '.$charged;
703         }       
704
705
706         if ($dec_inventory == '1') {
707 #Decrement item quantities in inventory table
708         my $dbi  = Vend::Data::database_exists_ref('inventory') or die errmsg("cannot open inventory table");
709         my $dbhi = dbconnectwp() or die errmsg("cannot get handle for tbl 'inventory'");
710         my ($sthi, $itm, $qty);
711         
712         foreach my $items (@{$acart})   { 
713                 $itm = $items->{'code'};
714                 $qty = $items->{'quantity'};
715                 $sthi = $dbh->prepare("UPDATE inventory SET quantity = quantity -'$qty' WHERE sku = '$itm'");
716                 $sthi->execute() or die errmsg("Cannot update table inventory");
717 #::logDebug("WP:".__LINE__.": Decremented inventory for $itm by $qty");
718         }
719 }
720
721 # run custom final route which cascades 'copy_user' and 'main_entry', ie no receipt page.
722         Vend::Order::route_order("wp_final", @cart) if @cart; 
723
724                 undef $Vend::Session->{mv_order_number};
725
726 #::logDebug("WP:".__LINE__.": sid=$::Session->{'id'};  End worldpay transaction success");
727 }
728
729 elsif ($callbackpw ne $callpw) { 
730 #This should never happen unless someone tries to simulate transactions without knowing the callback password or the password is entered incorrectly in catalog.cfg or the WP admin panel
731 #Transaction logged as txtype WP Pass Error and status passerror
732 #::logDebug("WP:".__LINE__.": Callback password was incorrect");
733 #::logDebug("WP:".__LINE__.": Logging transaction with callback password failure to tbl for order $wp_order_number");
734         $sql = "DELETE FROM transactions WHERE order_number='$wp_order_number'";
735         $sth = $dbh->prepare("$sql") or warn "sth failed\n";
736         $sth->execute() or die errmsg("Transactions tbl failed");
737         $sql = "DELETE FROM orderline WHERE order_number='$wp_order_number'";
738         $stho = $dbh->prepare("$sql") or warn "sth failed\n";
739         $stho->execute() or die errmsg("Orderline tbl failed");
740 }
741
742 else {
743 #transaction has been cancelled
744 #::logDebug("WP:".__LINE__.": Transaction for order $wp_order_number was cancelled");
745
746 #log details of cancelled transaction & set archived to 1 so it won't show in admin panel
747 #::logDebug("WP:".__LINE__.": Deleting cancelled transaction details from tbl for order $wp_order_number");
748 #    $dbh = dbconnectwp() or warn "dbh failed\n";
749         $sql = "DELETE FROM transactions WHERE order_number='$wp_order_number'";
750         $sth = $dbh->prepare("$sql") or warn "sth failed\n";
751         $sth->execute() or die errmsg("Transactions tbl failed");
752         $sql = "DELETE FROM orderline WHERE order_number='$wp_order_number'";
753         $sth = $dbh->prepare("$sql") or warn "sth failed\n";
754         $sth->execute() or die errmsg("Orderline tbl failed");
755         
756 #       $Vend::Session->{'payment_result'}{'MStatus'} = 'cancelled';
757         }
758
759   }
760         
761 }
762
763 package Vend::Payment::Worldpay;
764
765 1;
766