Fix handling of extra_query_params in Business::OnlinePayment wrapper.
[interchange.git] / lib / Vend / Payment / Protx2.pm
1 # Vend::Payment::Protx2 - Interchange Protx Direct payment support
2 #
3 # $Id: Protx2.pm,v 1.2 2008-04-10 23:44:45 jon Exp $
4 # Based on Protx2.pm, v 2.1.2, July 2007
5 #
6 # Copyright (C) 2008 Interchange Development Group
7 # Copyright (C) 2007 Zolotek Resources Ltd. All rights reserved.
8 #
9 # Author: 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 # License along with this program; if not, write to the Free
23 # Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston,
24 # MA  02110-1301  USA.
25
26 package Vend::Payment::Protx2;
27
28 =head1 NAME
29
30 Interchange Protx Direct payment system interface
31
32 =head1 PREREQUISITES
33
34 Net::SSLeay
35     or
36 LWP::UserAgent and Crypt::SSLeay
37
38 wget - a recent version built with SSL and supporting the 'connect' timeout function.
39
40 =head1 QUICK START SUMMARY
41
42 1. Call this module in interchange.cfg with:
43
44     Require module Vend::Payment::Protx2
45
46 2. Add into products/variable.txt (tab separated):
47
48     MV_PAYMENT_MODE   protx
49
50 3. Add a new route into catalog.cfg (options for the last entry in parentheses):
51
52     Route protx id YourProtxID
53     Route protx host ukvps.protx.com (ukvpstest.protx.com)
54     Route protx currency GBP (USD, EUR, others, defaults to GBP)
55     Route protx txtype PAYMENT (AUTHENTICATE, DEFERRED)
56     Route protx available yes (no, empty)
57     Route protx logzero yes (no, empty)
58     Route protx double_pay yes (no, empty)
59     Route protx logdir "path/to/log/dir"
60     Route protx protxlog yes (no, empty)
61     Route protx applyavscv2 '0': if enabled then check, and if rules apply use.
62                             '1': force checks even if not enabled; if rules apply use.
63                             '2': force NO checks even if enabled on account.
64                             '3': force checks even if not enabled; do NOT apply rules.
65     Route protx giftaidpayment 0 (1 to donate tax to Gift Aid)
66
67 or put these vars into products/variable.txt instead:
68
69     MV_PAYMENT_ID   YourProtxID Payment
70     MV_PAYMENT_MODE protx   Payment
71     MV_PAYMENT_HOST ukvps.protx.com Payment
72     MV_PAYMENT_CURRENCY GBP Payment
73
74 and the rest as above.
75
76 4. Create a new locale setting for en_UK as noted in "item currency" below, and copy the
77 public space interchange/en_US/ directory to a new interchange/en_UK/ one. Ensure that any
78 other locales you might use have a correctly named directory as well. Ensure that this locale
79 is found in your version of locale.txt (and set up UK as opposed to US language strings to taste).
80
81 5. Create entry boxes on your checkout page for: 'mv_credit_card_issue_number', 'mv_credit_card_start_month',
82 'mv_credit_card_start_year', 'mv_credit_card_type', and optionally 'mv_credit_card_cvv2'.
83
84 =head1 DESCRIPTION
85
86 The Vend::Payment::Protx module implements the Protx() routine for use with
87 Interchange. It is not compatible on a call level with the other Interchange
88 payment modules - Protx does things rather differently. We need to save four of
89 the returned codes for re-use when doing a RELEASE, REPEAT, or REFUND.
90
91 To enable this module, place this directive in C<interchange.cfg>:
92
93     Require module Vend::Payment::Protx2
94
95 This I<must> be in interchange.cfg or a file included from it.
96
97 Make sure CreditCardAuto is off (default in Interchange demos).
98
99 Note that the Protx 'Direct' system is the only one which leaves the customer on
100 your own site and takes payment in real time. Their other systems, eg Terminal
101 or Server, do not require this module.
102
103 Note also that Maestro cards can only be taken by the 3DSecure version of this module, not by this
104 version, as Mastercard have decreed that Maestro cards will no longer be accepted without 3DSecure.
105
106 While PREAUTH is still in this module, it is scheduled to be dropped on the 1st August 2007 or shortly
107 thereafter, and is only here as a backup during the changeover to AUTHENTICATE.
108
109 =head2 The active settings
110
111 The module uses several of the standard settings from the Interchange payment routes.
112 Any such setting, as a general rule, is obtained first from the tag/call options on
113 a page, then from an Interchange order Route named for the mode in catalog.cfg,
114 then a default global payment variable in products/variable.txt, and finally in
115 some cases a default will be hard-coded into the module.
116
117 =over
118
119 =item Mode
120
121 The mode can be named anything, but the C<gateway> parameter must be set
122 to C<protx>. To make it the default payment gateway for all credit card
123 transactions in a specific catalog, you can set in C<catalog.cfg>:
124
125     Variable  MV_PAYMENT_MODE  protx
126
127 or in variable.txt:
128
129     MV_PAYMENT_MODE protx (tab separated)
130
131 if you want this to cooperate with other payment systems, eg PaypalExpress or GoogleCheckout, then see
132 the documentation that comes with that system - it should be fully explained there (essentially, you
133 don't run the charge route from profiles.order but from log_transaction).
134
135 =item id
136
137 Your Protx vendor ID, supplied by Protx when you sign up. Various ways to state this:
138
139 in variable.txt:
140
141     MV_PAYMENT_ID   YourProtxID Payment
142
143 or in catalog.cfg either of:
144
145     Route protx id YourProtxID
146     Variable MV_PAYMENT_ID      YourProtxID
147
148 =item txtype
149
150 The transaction type is one of: PAYMENT, AUTHENTICATE or DEFERRED for an initial purchase
151 through the catalogue, and then can be one of: REFUND, RELEASE, REPEAT for payment
152 operations through the virtual terminal.
153
154 The transaction type is taken firstly from a dynamic variable in the page, meant
155 primarily for use with the 'virtual payment terminal', viz: 'transtype' in a select box
156 though this could usefully be taken from a possible entry in the products database
157 if you have different products to be sold on different terms; then falling back to
158 a 'Route txtype AUTHENTICATE' entry in catalog.cfg; then falling back to a global
159 variable in variable.txt, eg 'MV_PAYMENT_TXTYPE AUTHENTICATE Payment'; and finally
160 defaulting to 'PAYMENT' hard-coded into the module. This variable is returned to
161 the module and logged using the value returned from Protx, rather than a value from
162 the page which possibly may not exist.
163
164 =item available
165
166 If 'yes', then the module will check that the gateway is responding before sending the transaction.
167 If it fails to respond within 9 seconds, then the module will go 'off line' and log the transaction
168 as though this module had not been called. It will also log the txtype as 'OFFLINE' so that you
169 know you have to put the transaction through manually later (you will need to capture the card
170 number to do this). The point of this is that your customer has the transaction done and dusted,
171 rather than being told to 'try again later' and leaving for ever. If not explicitly 'yes',
172 defaults to 'no'. NB: if you set this to 'yes', then add into the etc/report that is sent to you:
173 Txtype = [calc]($Session->{payment_result} || {})->{TxType};[/calc]. Note that you need to have
174 a recent version of wget which supports '--connect-timeout' to run this check. Note also that,
175 as this transaction has not been logged anywhere on the Protx server, you cannot use their
176 terminal to process it. You must use the PTIPM which includes a function for this purpose; ie,
177 it updates the existing order number with the new payment information returned from Protx. Note
178 further that if you have Protx set up to require the CV2 value, then the PTIPM will disable
179 CV2 checking at run-time by default for such a transaction (logging the CV2 value breaks Visa/MC
180 rules and so it can't be legally available for this process).
181
182 =item logzero
183
184 If 'yes', then the module will log a transaction even if the amount sent is zero (which the
185 gateway would normally reject). The point of this is to allow a zero amount in the middle of a
186 subscription billing series for audit purposes. If not explicitly 'yes', defaults to 'no'.
187 Note: this is only useful if you are using an invoicing system or the Payment Terminal, both of which
188 by-pass the normal IC processes. IC will allow an item to be processed at zero total price but simply
189 bypasses the gateway when doing so.
190
191 =item logempty
192
193 If 'yes, then if the response from Protx is read as empty (ie, zero bytes) the result is forced to
194 'success' and the transaction logged as though the customer has paid. There are two markers set to
195 warn of this:
196
197 $Session->{payment_result}{TxType} will be NULL,
198
199 $Session->{payment_result}{StatusDetail} will be: 'UNKNOWN status - check with Protx before dispatching goods'
200
201 and you should include these into the report emailed to you.
202
203 =item card_type
204
205 Protx requires that the card type be sent. Valid types are: VISA, MC, AMEX, DELTA, SOLO, UKE,
206 JCB, DINERS (UKE is Visa Electron issued in the UK). MAESTRO is no longer accepted without 3DSecure.
207 This may optionally be determined by the module using regexes, or you may use a select box on the page.
208 If there is an error in the regex match in this module due to a change in card ranges or some other
209 fault, then Protx will refuse the transaction and return an error message to the page. Using a select
210 box on the page automatically overrides use of the internal option. In the interests of robust
211 reliability it is *strongly* recommended that you use a select box.
212
213 You may display a select box on the checkout page like so:
214
215     <select name="mv_credit_card_type">
216     [loop
217         option=mv_credit_card_type
218         acclist=1
219         list=|
220             VISA=Visa,
221             MC=MasterCard,
222             SOLO=Solo,
223             DELTA=Delta,
224             AMEX=Amex,
225             UKE=Electron,
226             JCB=JCB,
227             DINERS=Diners
228         |
229     ]
230         <option value="[loop-code]">[loop-param label]</option>
231     [/loop]
232     </select>
233
234 =item currency
235
236 Protx requires that a currency code be sent, using the 3 letter ISO standard,
237 eg, GBP, EUR, USD. The value is taken firstly from either a page setting or a
238 possible value in the products database, viz 'iso_currency_code'; then falling back
239 to the locale setting - for this you need to add to locale.txt:
240
241     code    en_UK   en_EUR  en_US
242     iso_currency_code   GBP EUR USD
243
244 It then falls back to a 'Route protx currency EUR' type entry in catalog.cfg;
245 then falls back to a global variable (eg MV_PAYMENT_CURRENCY EUR Payment); and
246 finally defaults to GBP hard-coded into the module. This variable is returned to
247 the module and logged using the value returned from Protx, rather than a value from
248 the page which possibly may not exist.
249
250 =item cvv2
251
252 This is sent to Protx as mv_credit_card_cvv2. Put this on the checkout page:
253
254     CVV2: <input type=text name=mv_credit_card_cvv2 value='' size=6>
255
256 but note that under PCI rules you must not log this value anywhere.
257
258 =item issue_number
259
260 This is used for some debit cards, and taken from an input box on the checkout page:
261
262     Card issue number: <input type=text name=mv_credit_card_issue_number value='' size=6>
263
264 =item StartDate
265
266 This is used for some debit cards, and is taken from select boxes on the
267 checkout page in a similar style to those for the card expiry date. The labels to be
268 used are: 'mv_credit_card_start_month', 'mv_credit_card_start_year'. Eg:
269
270     <select name="mv_credit_card_start_year">
271     [loop option=start_date_year lr=1 list=`
272         my $year = $Tag->time( '', { format => '%Y' }, '%Y' );
273         my $out = '';
274         for ($year - 7 .. $year) {
275                 /\d\d(\d\d)/;
276                 $last_two = $1;
277                 $out .= "$last_two\t$_\n";
278         }
279         return $out;
280     `]
281         <option value="[loop-code]">[loop-pos 1]</option>
282     [/loop]
283     </select>
284
285 =item Log directory
286
287 To choose the directory used for logging both the Protx latency log and the double
288 payment safeguard record, set in catalog.cfg:
289
290     Route protx logdir "path/to/log/dir"
291
292 It must be relative to the catalog root directory if you have
293 NoAbsolute set for this catalog in interchange.cfg.
294
295 If logdir is not set, it defaults to the system /tmp.
296
297 A somewhat dangerous option allows the payment page to specify the
298 logdir in a form variable, like this:
299
300     <input type="hidden" name="logdir" value='your_choice_here'>
301
302 This allows an individual user to have his own logs in a shared hosting
303 environment. However, it also allows a creative end-user to create
304 arbitrary empty files or update timestamps of existing files.
305
306 Because of the potential for abuse, this option is not allowed unless you set
307 a special route variable indicating you want it:
308
309     Route protx logdir_from_user_allowed 1
310
311 =item Protx API v2.22 extra functions
312
313 ApplyAVSCV2 set to:
314
315     0 = If AVS/CV2 enabled then check them. If rules apply, use rules. (default)
316     1 = Force AVS/CV2 checks even if not enabled for the account. If rules apply, use rules.
317     2 = Force NO AVS/CV2 checks even if enabled on account.
318     3 = Force AVS/CV2 checks even if not enabled for the account but DON'T apply any rules.
319     You may pass this value from the page as 'applyavscv2' to override the payment route setting.
320
321 CustomerName: optional, may be different to the cardholder name
322
323 ContactFax: optional
324
325 GiftAidPayment: set to -
326     0 = This transaction is not a Gift Aid charitable donation(default)
327     1 = This payment is a Gift Aid charitable donation and the customer has AGREED to donate the tax.
328     You may pass this value from the page as 'giftaidpayment'
329
330 ClientIPAddress: will show in Protx reports, and they will attempt to Geo-locate the IP.
331
332 =item Encrypted email with card info
333
334 If you want to add the extra fields (issue no, start date) to the PGP message
335 emailed back to you, then set the following in catalog.cfg:
336
337     Variable<tab>MV_CREDIT_CARD_INFO_TEMPLATE Card type: {MV_CREDIT_CARD_TYPE}; Card no: {MV_CREDIT_CARD_NUMBER}; Expiry: {MV_CREDIT_CARD_EXP_MONTH}/{MV_CREDIT_CARD_EXP_YEAR}; Issue no: {MV_CREDIT_CARD_ISSUE_NUMBER}; StartDate: {MV_CREDIT_CARD_START_MONTH}/{MV_CREDIT_CARD_START_YEAR}
338
339 =item testing
340
341 The Protx test site is ukvpstest.protx.com, and their live site is
342 ukvps.protx.com. Enable one of these in MV_PAYMENT_HOST in variable.txt
343 (*without* any leading https://) or as 'Route protx host ukvpstest.protx.com' in
344 catalog.cfg.
345
346 =item methods
347
348 NB: Protx have removed PREAUTH from their protocol and replaced it with AUTHENTICATE/AUTHORISE.
349
350 An AUTHENTICATE will validate the card and store the card details on Protx's system for up to 90 days.
351 Against this you may AUTHORISE for any amount up to 115% of the original value.
352
353 A DEFERRED will place a shadow ('block') on the funds for seven days (or so, depending
354 on the acquiring bank). Against a DEFERRED you may do a RELEASE to settle the transaction.
355
356 A PAYMENT will take the funds immediately. Against a PAYMENT, you may do a
357 REFUND or REPEAT.
358
359 A RELEASE is performed to settle a DEFERRED. Payment of the originally specified
360 amount is guaranteed if the RELEASE is performed within the seven days for which
361 the card-holder's funds are 'blocked'.
362
363 A REFUND may be performed against a PAYMENT, RELEASE, AUTHORISE or REPEAT. It may be for a
364 partial amount or the entire amount, and may be repeated with several partial
365 REFUNDs so long as the total does not exceed the original amount.
366
367 A DIRECTREFUND sends funds from your registered bank account to the nominated credit card.
368 This does not need to refer to any previous transaction codes, and is useful if you need to
369 make a refund but the customer's card has changed or the original purchase was not made by card.
370
371 =back
372
373 =head2 Virtual Payment Terminal
374
375 This has now been split out from this module, and may be found as the rather pretentiously named
376 Payment Terminal Interchange Plug-in Module (PTIPM), also on http://kiwi.zolotek.net. The PTIPM
377 does refunds and repeats, directrefunds, and converts offline transactions to online ones. Being a
378 plugin to the Interchange Admin Panel it integrates these operations into your database.
379
380 =head1 TROUBLESHOOTING
381
382 Only the test card numbers given below will be successfully
383 authorised (all other card numbers will be declined).
384
385     VISA                    4929 0000 0000 6
386     MASTERCARD              5404 0000 0000 0001
387     DELTA                   4462000000000003
388     SOLO                    6334900000000005      issue 1
389     DOMESTIC MAESTRO        5641 8200 0000 0005   issue 01 (should be rejected now)
390     AMEX                    3742 0000 0000 004
391     ELECTRON                4917 3000 0000 0008
392     JCB                     3569 9900 0000 0009
393     DINERS                  3600 0000 0000 08
394
395 You'll also need to supply the following values for CV2, Billing Address Numbers and Billing Post Code
396 Numbers. These are the only values which will return as Matched on the test server. Any other values
397 will return a Not Matched on the test server.
398
399     CV2                        123
400     Billing Address Numbers    88
401     Billing Post Code Numbers  412
402
403 If nothing works:
404
405 =over 4
406
407 =item *
408
409 Make sure you "Require"d the module in interchange.cfg:
410
411     Require module Vend::Payment::Protx2
412
413 =item *
414
415 Make sure either Net::SSLeay or Crypt::SSLeay and LWP::UserAgent are installed
416 and working. You can test to see whether your Perl thinks they are:
417
418     perl -MNet::SSLeay -e 'print "It works\n"'
419 or
420     perl -MLWP::UserAgent -MCrypt::SSLeay -e 'print "It works\n"'
421
422 If either one prints "It works." and returns to the prompt you should be OK
423 (presuming they are in working order otherwise).
424
425 =item *
426
427 Check the error logs, both catalogue and global. Make sure you set your payment
428 parameters properly. Try an order, then put this code in a page:
429
430     <pre>
431     [calcn]
432         my $string = $Tag->uneval( { ref => $Session->{payment_result} });
433         $string =~ s/{/{\n/;
434         $string =~ s/,/,\n/g;
435         return $string;
436     [/calcn]
437     </pre>
438
439 That should show what happened.
440
441 =item *
442
443 If you have unexplained and unlogged errors then check you have allowed the new database fields to
444 be NULL. If MySQL tries to write to a field that is marked NOT NULL then it will fail silently.
445
446 =item *
447
448 If you have a PGP/GPG failure when placing an order through your catalogue
449 then this may cause the module to be immediately re-run. As the first run would
450 have been successful, meaning that both the basket and the credit card information
451 would have been emptied, the second run will fail. The likely error message within
452 the catalogue will be:
453 "Can't figure out credit card expiration". Fixing PGP/GPG will fix this error.
454
455 If you get the same error message within the Virtual Terminal, then you haven't
456 set the order route as noted above.
457
458 =item *
459
460 If all else fails, Zolotek and other consultants are available to help
461 with integration for a fee.
462
463 =back
464
465 =head1 RESOURCES
466
467 http://kiwi.zolotek.net is the home page with the latest version. Also to be found on
468 Kevin Walsh's excellent Interchange site, http://interchange.rtfm.info.
469
470 =head1 AUTHORS
471
472 Lyn St George <info@zolotek.net>, based on original code by Mike Heins
473 <mike@perusion.com> and others.
474
475 =head1 CREDITS
476
477 Hillary Corney (designersilversmiths.co.uk), Jamie Neil (versado.net),
478 Andy Mayer (andymayer.net) for testing and suggestions.
479
480 =head1 LICENSE
481
482 GPLv2
483
484 =cut
485
486 use strict;
487
488 BEGIN {
489
490     my $selected;
491     eval {
492         package Vend::Payment;
493         require Net::SSLeay;
494         import Net::SSLeay qw(post_https make_form make_headers);
495         $selected = "Net::SSLeay";
496     };
497
498     $Vend::Payment::Have_Net_SSLeay = 1 unless $@;
499
500     unless ($Vend::Payment::Have_Net_SSLeay) {
501
502         eval {
503             package Vend::Payment;
504             require LWP::UserAgent;
505             require HTTP::Request::Common;
506             require Crypt::SSLeay;
507             import HTTP::Request::Common qw(POST);
508             $selected = "LWP and Crypt::SSLeay";
509         };
510
511         $Vend::Payment::Have_LWP = 1 unless $@;
512     }
513
514     unless ($Vend::Payment::Have_Net_SSLeay or $Vend::Payment::Have_LWP) {
515         die __PACKAGE__ . " requires Net::SSLeay or Crypt::SSLeay";
516     }
517
518     ::logGlobal("%s v2.1.2 payment module initialised, using %s", __PACKAGE__, $selected)
519         unless $Vend::Quiet;
520
521 }
522
523 package Vend::Payment;
524
525 sub protx {
526
527     my ($vendor, $amount, $actual, $opt);
528
529     # Amount sent to Protx, in 2 decimal places with any cruft removed.
530     # Defaults to 'amount' from the Accounts IPM or an invoicing system, falling back to IC input
531     $amount =  $::Values->{amount} || Vend::Interpolate::total_cost();
532     $amount =~ s/^\D+//g;
533     $amount =~ s/,//g;
534     $amount =  sprintf '%.2f', $amount;
535
536     # Transaction type sent to Protx.
537     my $txtype = $::Values->{transtype} || charge_param('txtype') || $::Variable->{MV_PAYMENT_TRANSACTION} || 'PAYMENT';
538     my $accountType = $::Values->{account_type} || charge_param('account_type') || 'E';
539     my $payID  = $::Values->{inv_no} || $::Session->{mv_transaction_id} || $::Session->{id}.$amount;
540
541     my $logdir;
542
543     # is logdir allowed to come from user?
544     if (charge_param('logdir_from_user_allowed')) {
545         $logdir = $::Values->{logdir};
546     }
547     elsif ($::Values->{logdir}) {
548         ::logError("%s: user-specified logdir not allowed without route logdir_from_user 1", __PACKAGE__);
549     }
550
551     # was logdir specified in route?
552     $logdir ||= charge_param('logdir');
553     my $default_logdir = '/tmp';
554     if (! $logdir) {
555         $logdir = $default_logdir;
556     }
557     # validate logdir is valid
558     elsif (! Vend::File::allowed_file("$logdir/TEST_FILE_NAME")) {
559         ::logError("%s: using logdir %s instead of disallowed %s", __PACKAGE__, $default_logdir, $logdir);
560         $logdir = $default_logdir;
561     }
562     $logdir = Vend::File::make_absolute_file($logdir);
563
564     my $logzero    = charge_param('logzero')    || 'no';
565     my $available  = charge_param('available')  || 'no';
566     my $logempty   = $::Values->{logempty} || charge_param('logempty') || 'no';
567     my $double_pay = $::Values->{double_pay} || charge_param('double_pay') || 'no';
568     my $findcard   = charge_param('find_card_type') || 'no'; # yes for auto, page for input, no for IC
569     my $description = charge_param('description') || $::Variable->{COMPANY};
570     $description = substr($description,0,99);
571     my $applyAVSCV2 = $::Values->{applyavscv2} || charge_param('applyavscv2') || '0';
572
573     # if payment is logged as made, raise an error message and exit
574     my $marker;
575     if ($txtype =~ /DEFERRED/i) {
576         $marker = "$logdir/pre-$payID";
577     }
578     else {
579         $marker = "$logdir/paid-$payID";
580     }
581
582     my %result;
583
584     # check for double payment only if using the payment terminal or making an invoice payment. Allow the
585     # payment terminal to override this check to 'off' and allow identical amounts to be processed within
586     # the same session.
587     if (($::Values->{mv_order_route} =~ /ptipm_route|protx_vt_route/i) and (-e $marker) and ($double_pay eq 'yes')) {
588         unless ($txtype =~ /REFUND|VOID|ABORT/) {
589             $result{MErrMsg} = "Payment for this transaction $marker has already been made - thank you";
590             return %result;
591         }
592     }
593     # wrap around everything to bottom
594     else {
595         my %actual = map_actual();
596         $actual  = \%actual;
597         $opt     = {};
598
599 #::logDebug("actual map result: " . ::uneval($actual));
600         $vendor   = $opt->{id} || charge_param('id') || $::Variable->{MV_PAYMENT_ID};
601         $opt->{host}   = charge_param('host') || $::Variable->{MV_PAYMENT_HOST} || 'ukvpstest.protx.com';
602         $opt->{use_wget} = charge_param('use_wget') || '1';
603         $opt->{port}   = '443';
604
605         if ($txtype =~ /DEFERRED|PAYMENT|AUTHENTICATE|PREAUTH/i) {
606             $opt->{script} = '/vspgateway/service/vspdirect-register.vsp';
607         }
608         elsif ($txtype =~ /RELEASE/i) {
609             $opt->{script} = '/vspgateway/service/release.vsp';
610         }
611         elsif ($txtype =~ /DIRECTREFUND/i) {
612             $opt->{script} = '/vspgateway/service/directrefund.vsp';
613         }
614         elsif ($txtype =~ /REFUND/i) {
615             $opt->{script} = '/vspgateway/service/refund.vsp';
616         }
617         elsif ($txtype =~ /VOID/i) {
618             $opt->{script} = '/vspgateway/service/void.vsp';
619         }
620         elsif ($txtype =~ /CANCEL/i) {
621             $opt->{script} = '/vspgateway/service/cancel.vsp';
622         }
623         elsif ($txtype =~ /ABORT/i) {
624             $opt->{script} = '/vspgateway/service/abort.vsp';
625         }
626         elsif ($txtype =~ /MANUAL/i) {
627             $opt->{script} = '/vspgateway/service/manualpayment.vsp';
628         }
629         elsif ($txtype =~ /REPEAT|REPEATDEFERRED/i) {
630             $opt->{script} = '/vspgateway/service/repeat.vsp';
631         }
632         elsif ($txtype =~ /AUTHORISE/i) {
633             $opt->{script} = '/vspgateway/service/authorise.vsp';
634         }
635
636         my @override = qw/
637             order_id
638             mv_credit_card_exp_month
639             mv_credit_card_exp_year
640             mv_credit_card_start_month
641             mv_credit_card_start_year
642             mv_credit_card_issue_number
643             mv_credit_card_number
644         /;
645         for(@override) {
646             next unless defined $opt->{$_};
647             $actual->{$_} = $opt->{$_};
648         }
649
650         my $ccnum = $actual->{mv_credit_card_number};
651         $ccnum =~ s/\D//g;
652         $actual->{mv_credit_card_exp_month}    =~ s/\D//g;
653         $actual->{mv_credit_card_exp_year}     =~ s/\D//g;
654         $actual->{mv_credit_card_exp_year}     =~ s/\d\d(\d\d)/$1/;
655
656         my $startDateMonth = $actual->{mv_credit_card_start_month} || $::Values->{mv_credit_card_start_month} || $::Values->{start_date_month} || 01;
657         $startDateMonth =~ s/\D//g;
658
659         my $startDateYear = $actual->{mv_credit_card_start_year} || $::Values->{mv_credit_card_start_year} || $::Values->{start_date_year} || 06;
660         $startDateYear =~ s/\D//g;
661         $startDateYear =~ s/\d\d(\d\d)/$1/;
662
663         my $issue = $actual->{mv_credit_card_issue_number} || $::Values->{mv_credit_card_issue_number} || $::Values->{card_issue_number};
664         $issue =~ s/\D//g;
665
666         my $cvv2  = $actual->{mv_credit_card_cvv2} || $::Values->{cvv2};
667         $cvv2  =~ s/\D//g;
668
669         # overide the configured AVSCV2 setting
670         if($txtype =~ /REPEAT|RELEASE|REFUND/i) {
671             $applyAVSCV2 = '2';
672         }
673
674         my $exp = sprintf '%02d%02d', $actual->{mv_credit_card_exp_month}, $actual->{mv_credit_card_exp_year};
675
676         my $startDate;
677         if (!$startDateMonth) {
678             $startDate = '';
679         }
680         else {
681             $startDate = sprintf '%02d%02d', $startDateMonth, $startDateYear;
682         }
683
684         my $cardType;
685
686         if ($::Values->{mv_credit_card_type}) {
687             $cardType = $::Values->{mv_credit_card_type};
688         }
689         else {
690             if ($ccnum =~ /^4(?:5085[0-9]|91880)\d{10}$/)                     {$cardType = 'UKE'}
691             elsif ($ccnum =~ /^4917(?:3[0-3]|4(?:[0-2]|[9])|5[28])\d{10}$/)   {$cardType = 'UKE'}
692
693             elsif ($ccnum =~ /^4462[0-9][0-9]\d{10}$/)                        {$cardType = 'Delta'}
694             elsif ($ccnum =~ /^45(?:397[89]|4313|443[2-5])\d{10}$/)           {$cardType = 'Delta'}
695             elsif ($ccnum =~ /^4547(?:[2][5-9]|[3][0-9]|[4][0-5])\d{10}$/)    {$cardType = 'Delta'}
696             elsif ($ccnum =~ /^49(?:09[67][0-9]|218[12]|8824)\d{10}$/)        {$cardType = 'Delta'}
697
698             elsif ($ccnum =~ /^6011\d{12}$/)                                  {$cardType = 'Discover'}
699             elsif ($ccnum =~ /^3(?:6\d{12}|0[0-5]\d{11})$/)                   {$cardType = 'Dinersclub'}
700             elsif ($ccnum =~ /^38\d{12}$/)                                    {$cardType = 'Carteblanche'}
701             elsif ($ccnum =~ /^2(?:014|149)\d{11}$/)                          {$cardType = 'Enroute'}
702             elsif ($ccnum =~ /^(?:3\d{15}|2131\d{11}|1800\d{11})$/)           {$cardType = 'JCB'}
703
704             elsif ($ccnum =~ /^490(?:30[2-9]|33[5-9]|340|52[5-9])\d{10,12}$/) {$cardType = 'MAESTRO'}
705             elsif ($ccnum =~ /^4911(?:0[0-2]|7[4-9]|8[0-2])\d{12,14}$/)       {$cardType = 'MAESTRO'}
706             elsif ($ccnum =~ /^4936[0-9][0-9]\d{10,13}$/)                     {$cardType = 'MAESTRO'}
707             elsif ($ccnum =~ /^564182\d{10}$/)                                {$cardType = 'MAESTRO'}
708             elsif ($ccnum =~ /^633(?:110|3[0-9][0-9]|461)\d{10,12,13}$/)      {$cardType = 'MAESTRO'}
709             elsif ($ccnum =~ /^6759\d{12,14,15}$/)                            {$cardType = 'MAESTRO'}
710
711             elsif ($ccnum =~ /^49030[2-9]\d{12}$/)                            {$cardType = 'Solo'}
712             elsif ($ccnum =~ /^63345[0-9]\d{10}$/)                            {$cardType = 'Solo'}
713             elsif ($ccnum =~ /^63346([0]|[2-9])\d{10}$/)                      {$cardType = 'Solo'}
714             elsif ($ccnum =~ /^6334[7-9][0-9]\d{10,12,13}$/)                  {$cardType = 'Solo'}
715             elsif ($ccnum =~ /^6767[0-9][0-9]\d{10,12,13}$/)                  {$cardType = 'Solo'}
716
717             elsif ($ccnum =~ /^4(?:\d{12}|\d{15})$/)                          {$cardType = 'Visa'}
718             elsif ($ccnum =~ /^5[1-5]\d{14}$/)                                {$cardType = 'MC'}
719             elsif ($ccnum =~ /^3[47]\d{13}$/)                                 {$cardType = 'Amex'}
720         }
721
722         # Mastercard require Maestro to use 3ds now.
723         if ($cardType =~ /Switch|Maestro/i) {
724             $result{MStatus} = $result{'pop.status'} = 'failed';
725             $result{MErrMsg} = "Sorry, we do not accept Maestro cards";
726             return %result;
727         }
728
729         my $cardRef = $actual->{mv_credit_card_number};
730         $cardRef =~ s/^(\d\d).*(\d\d\d\d)$/$1****$2/;
731
732         # Prefer billing values but fall back to shipping values.
733         my $billingAddress  = sprintf '%s, %s, %s, %s',
734             $actual->{b_address} || $actual->{address},
735             $actual->{b_city}    || $actual->{city},
736             $actual->{b_state}   || $actual->{state},
737             $actual->{b_country} || $actual->{country};
738
739         my $deliveryAddress = sprintf '%s, %s, %s, %s',
740             $actual->{address},
741             $actual->{city},
742             $actual->{state},
743             $actual->{country};
744
745         my $cardHolder = $actual->{b_name} || '$actual->{b_fname} $actual->{b_lname}'
746             || $actual->{name} || '$actual->{fname} $actual->{lname}';
747
748         my $billingPostCode   = $actual->{b_zip}  || $actual->{zip};
749         my $deliveryPostCode  = $actual->{zip}    || $actual->{b_zip};
750         my $customerName      = $actual->{name}   || '$actual->{fname} $actual->{lname}' || $cardHolder;
751         my $contactNumber     = $actual->{phone_day} || $actual->{phone_night};
752         my $customerEmail     = $actual->{email};
753         my $contactFax        = $::Values->{fax} || '';
754         my $giftAidPayment    = $::Values->{giftaidpayment} || charge_param('giftaidpayment') || '0';
755         my $authCode          = $::Values->{authcode} || '';
756         my $clientIPAddress   = $CGI::remote_addr;
757
758         # VendorTxCode generated here.
759         my $vendorTxCode;
760         my $order_id = gen_order_id($opt);
761         if ($txtype =~ /RELEASE|VOID|ABORT/i) {
762             $vendorTxCode = $::Values->{OrigVendorTxCode};
763         }
764         else {
765             $vendorTxCode = $order_id;
766         }
767
768         # ISO currency code sent to Protx, from the page or fall back to config files.
769         my $currency = $::Values->{iso_currency_code} || $::Values->{currency_code} || $Vend::Cfg->{Locale}{iso_currency_code}
770             || charge_param('currency') || $::Variable->{MV_PAYMENT_CURRENCY} || 'GBP';
771
772         my $psp_host = $opt->{host};
773
774         # The string sent to Protx.
775         my %query = (
776             TxType                      => $txtype,
777             VendorTxCode                => $vendorTxCode,
778             Vendor                      => $vendor,
779             AccountType                 => $accountType,
780             VPSProtocol                 => '2.22',
781             Apply3DSecure               => '2',
782         );
783         if ($txtype =~ /REFUND|REPEAT|AUTHORISE/) {
784             $query{RelatedVPSTxID}      = $::Values->{RelatedVPSTxID};
785             $query{RelatedVendorTxCode} = $::Values->{RelatedVendorTxCode};
786             $query{RelatedSecurityKey}  = $::Values->{RelatedSecurityKey};
787             $query{Description}         = $description;
788             $query{Amount}              = $amount;
789         }
790         if ($txtype =~ /REFUND|REPEAT/) {
791             $query{RelatedTxAuthNo}     = $::Values->{RelatedTxAuthNo};
792             $query{Currency}            = $currency;
793         }
794         if ($txtype =~ /VOID|ABORT|CANCEL/i) {
795             $query{VPSTxID}             = $::Values->{OrigVPSTxID};
796             $query{SecurityKey}         = $::Values->{OrigSecurityKey};
797         }
798         if ($txtype =~ /VOID|ABORT/i) {
799             $query{TxAuthNo}            = $::Values->{OrigTxAuthNo};
800         }
801         if ($txtype =~ /DIRECTREFUND|DEFERRED|PAYMENT|PREAUTH|AUTHENTICATE|MANUAL/i) {
802             $query{CardType}            = $cardType;
803             $query{CardNumber}          = $ccnum;
804             $query{IssueNumber}         = $issue;
805             $query{CardHolder}          = $cardHolder;
806             $query{Description}         = $description;
807             $query{Amount}              = $amount;
808             $query{Currency}            = $currency;
809             $query{StartDate}           = $startDate;
810             $query{ExpiryDate}          = $exp
811         }
812         if ($txtype =~ /PAYMENT|DEFERRED|PREAUTH|AUTHENTICATE|MANUAL/i) {
813             $query{BillingAddress}      = $billingAddress;
814             $query{DeliveryAddress}     = $deliveryAddress;
815             $query{BillingPostCode}     = $billingPostCode;
816             $query{DeliveryPostCode}    = $deliveryPostCode;
817             $query{CustomerName}        = $customerName;
818             $query{ContactNumber}       = $contactNumber;
819             $query{ContactFax}          = $contactFax;
820             $query{CustomerEmail}       = $customerEmail;
821             $query{GiftAidPayment}      = $giftAidPayment;
822             $query{ClientIPAddress}     = $clientIPAddress;
823             $query{AuthCode}            = $authCode;
824             $query{CV2}                 = $cvv2;
825         }
826         if ($txtype =~ /PAYMENT|DEFERRED|PREAUTH|AUTHENTICATE|AUTHORISE/i) {
827             $query{ApplyAVSCV2}         = $applyAVSCV2;
828         }
829
830 #::logDebug("Sent to Protx: " . ::uneval(\%query));
831
832         # Test for gateway availability, and if not available optionally go off-line and complete
833         # transaction for manual processing later. Also go off-line if amount is zero, so as to log the
834         # transaction and email a receipt for audit purposes (useful mainly for subscription billing).
835
836         my ($request, $in);
837
838 #::logDebug("Protx809: available=$available, amount=$amount, order_route=$::Values->{mv_order_route}, logzero=$logzero\n");
839
840         if (($available eq 'yes') and ($amount > 0) and ($::Values->{mv_order_route} !~ /ptipm_route|protx_vt_route/i)) {
841             my $CMD = '/usr/bin/wget -nv --spider -T9 -t1';
842             open (my $in, "$CMD https://$psp_host 2>&1 |") || die "Could not open pipe to wget: $!\n";
843             $in = <$in>;
844             close $in;
845             # $in = 'test';   # testing only, will force offline mode
846             if ($in =~ /^200 OK$/) {
847                 $request = 'psp';
848             }
849             else {
850                 $request = 'offline';
851             }
852         }
853         elsif (($::Values->{mv_order_route} =~ /ptipm_route|protx_vt_route/i)  and ($amount > 0)) {
854             $request = 'psp';
855         }
856         elsif (($available ne 'yes') and ($amount > 0)) {
857             $request = 'psp';
858         }
859         elsif (($amount == 0) and ($logzero eq 'yes')) {
860             $request = 'log';
861         }
862
863         if ($request eq 'psp') {
864
865             my $post     = post_data($opt, \%query);
866             my $response = $post->{status_line};
867             my $page     = $post->{result_page};
868
869 #::logDebug("Response from Protx:\n$page \nend of response from Protx\n\n");
870
871             $result{TxType}         = $txtype;
872             $result{Currency}       = $currency;
873             $result{CardRef}        = $cardRef;
874             $result{CardType}       = $cardType;
875             $result{ExpAll}         = $exp;
876             $result{amount}         = $amount;
877             $result{request}        = $request;
878             $result{IP}             = $clientIPAddress;
879             if ($cardType eq 'UKE') { $result{CardType} = 'Electron'; }
880             $result{CardInfo}       = "$result{CardType}, $cardRef, $actual->{mv_credit_card_exp_month}/$actual->{mv_credit_card_exp_year}";
881
882             for my $line (split /\r\n/, $page) {
883                 if ($line =~ /VPSTxID=(.*)/i) { $result{VPSTxID} = $1; }
884                 if ($line =~ /^Status=(.*)/i) { $result{Status} = $1; }
885                 if ($line =~ /StatusDetail=(.*)/i) { $result{StatusDetail} = $1; }
886                 if ($line =~ /TxAuthNo=(.*)/i) { $result{TxAuthNo} = $1;}
887                 if ($line =~ /SecurityKey=(.*)/i) { $result{SecurityKey} = $1; }
888                 if ($line =~ /AVSCV2=(.*)/i) { $result{AVSCV2} = $1; }
889                 if ($line =~ /VPSProtocol=(.*)/i) { $result{VPSProtocol} = $1; }
890                 if ($line =~ /AddressResult=(.*)/i) { $result{AddressResult} = $1; }
891                 if ($line =~ /PostCodeResult=(.*)/i) { $result{PostCodeResult} = $1; }
892                 if ($line =~ /CV2Result=(.*)/i) { $result{CV2Result} = $1; }
893             }
894
895             if ($txtype =~ /PAYMENT|DEFERRED|MANUAL|PREAUTH|AUTHENTICATE|AUTHORISE/i) {
896
897                 if ($result{Status} =~ /OK$|REGISTERED/i) {
898                     $result{MStatus} = $result{'pop.status'} = 'success';
899                     $result{'order-id'} ||= $opt->{order_id};
900                 }
901                 elsif ($result{Status} !~ /OK$|REGISTERED/i)  {
902                     $result{MStatus} = $result{'pop.status'} = 'failed';
903                     if ($result{StatusDetail} =~ /AVS/i) {
904                         $result{MErrMsg} =  "Data mismatch<br>Please ensure that your address matches the address on your credit-card statement, and that the card security number matches the code on the back of your card\n";
905                     }
906                     else {
907                         $result{MErrMsg} = "$result{StatusDetail} $result{Status}";
908                     }
909                 }
910                 elsif (!$result{Status}) {
911                 # If the status response is read as empty, the customer will try to repeat the order. This next
912                 # will force the transaction to success in this case, thus preventing any attempt to repeat the
913                 # order, but flag it so that the merchant knows to manually check with Protx to see if they have
914                 # a valid transaction. There are different views on this approach - use with care.
915                     if ($logempty eq 'yes') {
916                         $result{MStatus} = $result{'pop.status'} = 'success';
917                         $result{'order-id'} ||= $opt->{order_id};
918                         $result{TxType} = 'NULL';
919                         $result{StatusDetail} = 'UNKNOWN status - check with Protx before dispatching goods';
920                     }
921                 }
922                 else {
923                     $result{MStatus} = $result{'pop.status'} = 'failed';
924                     $result{MErrMsg} = "$result{StatusDetail}";
925                 }
926             }
927
928             # these next can only be done through a virtual terminal
929             elsif($txtype =~ /RELEASE|REPEAT|REFUND|DIRECTREFUND|VOID|ABORT/i ) {
930                 if ($result{Status} =~ /OK|REGISTERED/i) {
931                     $result{MStatus} = $result{'pop.status'} = 'success';
932                     $result{'order-id'} ||= $opt->{order_id};
933                 }
934                 else {
935                     $result{MStatus} = $result{'pop.status'} = 'failed';
936                     $result{MErrMsg} = "$result{Status} $result{StatusDetail}";
937                 }
938             }
939         }
940         elsif ($request eq 'offline') {
941             # end of PSP request, now for either OFFLINE or LOG
942             # force result to 'success' so that transaction completes off-line
943             $result{MStatus} = 'success';
944             $result{'order-id'} ||= $opt->{order_id};
945             $result{TxType} = 'OFFLINE';
946         }
947         elsif ($request eq 'log') {
948             # force result to 'success' so that transaction completes off-line
949             $result{MStatus} = 'success';
950             $result{'order-id'} ||= $opt->{order_id};
951             $result{TxType} = 'LOG';
952         }
953
954         # if payment is confirmed as OK by Protx, log this for checking above
955 #        if ($result{Status} =~ /OK|REGISTERED/i) {
956             if (open my $touch, '>>', $marker) {
957                 close $touch;
958             }
959             else {
960                 ::logError("%s: error updating timestamp of %s: %s", __PACKAGE__, $marker, $!);
961             }
962 #        }
963
964     } # close double payment marker
965
966     return %result;
967 }
968
969 package Vend::Payment::Protx2;
970
971 1;